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 SelectedFormatter,
31 },
32 tree_sitter_python,
33};
34use language_settings::Formatter;
35use lsp::CompletionParams;
36use multi_buffer::{IndentGuide, PathKey};
37use parking_lot::Mutex;
38use pretty_assertions::{assert_eq, assert_ne};
39use project::{
40 FakeFs,
41 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
42 project_settings::LspSettings,
43};
44use serde_json::{self, json};
45use settings::{
46 AllLanguageSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring,
47 ProjectSettingsContent,
48};
49use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
50use std::{
51 iter,
52 sync::atomic::{self, AtomicUsize},
53};
54use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
55use text::ToPoint as _;
56use unindent::Unindent;
57use util::{
58 assert_set_eq, path,
59 rel_path::rel_path,
60 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
61 uri,
62};
63use workspace::{
64 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
65 OpenOptions, ViewId,
66 invalid_buffer_view::InvalidBufferView,
67 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
68 register_project_item,
69};
70
71#[gpui::test]
72fn test_edit_events(cx: &mut TestAppContext) {
73 init_test(cx, |_| {});
74
75 let buffer = cx.new(|cx| {
76 let mut buffer = language::Buffer::local("123456", cx);
77 buffer.set_group_interval(Duration::from_secs(1));
78 buffer
79 });
80
81 let events = Rc::new(RefCell::new(Vec::new()));
82 let editor1 = cx.add_window({
83 let events = events.clone();
84 |window, cx| {
85 let entity = cx.entity();
86 cx.subscribe_in(
87 &entity,
88 window,
89 move |_, _, event: &EditorEvent, _, _| match event {
90 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
91 EditorEvent::BufferEdited => {
92 events.borrow_mut().push(("editor1", "buffer edited"))
93 }
94 _ => {}
95 },
96 )
97 .detach();
98 Editor::for_buffer(buffer.clone(), None, window, cx)
99 }
100 });
101
102 let editor2 = cx.add_window({
103 let events = events.clone();
104 |window, cx| {
105 cx.subscribe_in(
106 &cx.entity(),
107 window,
108 move |_, _, event: &EditorEvent, _, _| match event {
109 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
110 EditorEvent::BufferEdited => {
111 events.borrow_mut().push(("editor2", "buffer edited"))
112 }
113 _ => {}
114 },
115 )
116 .detach();
117 Editor::for_buffer(buffer.clone(), None, window, cx)
118 }
119 });
120
121 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
122
123 // Mutating editor 1 will emit an `Edited` event only for that editor.
124 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
125 assert_eq!(
126 mem::take(&mut *events.borrow_mut()),
127 [
128 ("editor1", "edited"),
129 ("editor1", "buffer edited"),
130 ("editor2", "buffer edited"),
131 ]
132 );
133
134 // Mutating editor 2 will emit an `Edited` event only for that editor.
135 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
136 assert_eq!(
137 mem::take(&mut *events.borrow_mut()),
138 [
139 ("editor2", "edited"),
140 ("editor1", "buffer edited"),
141 ("editor2", "buffer edited"),
142 ]
143 );
144
145 // Undoing on editor 1 will emit an `Edited` event only for that editor.
146 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
147 assert_eq!(
148 mem::take(&mut *events.borrow_mut()),
149 [
150 ("editor1", "edited"),
151 ("editor1", "buffer edited"),
152 ("editor2", "buffer edited"),
153 ]
154 );
155
156 // Redoing on editor 1 will emit an `Edited` event only for that editor.
157 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
158 assert_eq!(
159 mem::take(&mut *events.borrow_mut()),
160 [
161 ("editor1", "edited"),
162 ("editor1", "buffer edited"),
163 ("editor2", "buffer edited"),
164 ]
165 );
166
167 // Undoing on editor 2 will emit an `Edited` event only for that editor.
168 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
169 assert_eq!(
170 mem::take(&mut *events.borrow_mut()),
171 [
172 ("editor2", "edited"),
173 ("editor1", "buffer edited"),
174 ("editor2", "buffer edited"),
175 ]
176 );
177
178 // Redoing on editor 2 will emit an `Edited` event only for that editor.
179 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
180 assert_eq!(
181 mem::take(&mut *events.borrow_mut()),
182 [
183 ("editor2", "edited"),
184 ("editor1", "buffer edited"),
185 ("editor2", "buffer edited"),
186 ]
187 );
188
189 // No event is emitted when the mutation is a no-op.
190 _ = editor2.update(cx, |editor, window, cx| {
191 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
192 s.select_ranges([0..0])
193 });
194
195 editor.backspace(&Backspace, window, cx);
196 });
197 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
198}
199
200#[gpui::test]
201fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
202 init_test(cx, |_| {});
203
204 let mut now = Instant::now();
205 let group_interval = Duration::from_millis(1);
206 let buffer = cx.new(|cx| {
207 let mut buf = language::Buffer::local("123456", cx);
208 buf.set_group_interval(group_interval);
209 buf
210 });
211 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
212 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
213
214 _ = editor.update(cx, |editor, window, cx| {
215 editor.start_transaction_at(now, window, cx);
216 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
217 s.select_ranges([2..4])
218 });
219
220 editor.insert("cd", window, cx);
221 editor.end_transaction_at(now, cx);
222 assert_eq!(editor.text(cx), "12cd56");
223 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
224
225 editor.start_transaction_at(now, window, cx);
226 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
227 s.select_ranges([4..5])
228 });
229 editor.insert("e", window, cx);
230 editor.end_transaction_at(now, cx);
231 assert_eq!(editor.text(cx), "12cde6");
232 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
233
234 now += group_interval + Duration::from_millis(1);
235 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
236 s.select_ranges([2..2])
237 });
238
239 // Simulate an edit in another editor
240 buffer.update(cx, |buffer, cx| {
241 buffer.start_transaction_at(now, cx);
242 buffer.edit([(0..1, "a")], None, cx);
243 buffer.edit([(1..1, "b")], None, cx);
244 buffer.end_transaction_at(now, cx);
245 });
246
247 assert_eq!(editor.text(cx), "ab2cde6");
248 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
249
250 // Last transaction happened past the group interval in a different editor.
251 // Undo it individually and don't restore selections.
252 editor.undo(&Undo, window, cx);
253 assert_eq!(editor.text(cx), "12cde6");
254 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
255
256 // First two transactions happened within the group interval in this editor.
257 // Undo them together and restore selections.
258 editor.undo(&Undo, window, cx);
259 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
260 assert_eq!(editor.text(cx), "123456");
261 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
262
263 // Redo the first two transactions together.
264 editor.redo(&Redo, window, cx);
265 assert_eq!(editor.text(cx), "12cde6");
266 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
267
268 // Redo the last transaction on its own.
269 editor.redo(&Redo, window, cx);
270 assert_eq!(editor.text(cx), "ab2cde6");
271 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
272
273 // Test empty transactions.
274 editor.start_transaction_at(now, window, cx);
275 editor.end_transaction_at(now, cx);
276 editor.undo(&Undo, window, cx);
277 assert_eq!(editor.text(cx), "12cde6");
278 });
279}
280
281#[gpui::test]
282fn test_ime_composition(cx: &mut TestAppContext) {
283 init_test(cx, |_| {});
284
285 let buffer = cx.new(|cx| {
286 let mut buffer = language::Buffer::local("abcde", cx);
287 // Ensure automatic grouping doesn't occur.
288 buffer.set_group_interval(Duration::ZERO);
289 buffer
290 });
291
292 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
293 cx.add_window(|window, cx| {
294 let mut editor = build_editor(buffer.clone(), window, cx);
295
296 // Start a new IME composition.
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 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
300 assert_eq!(editor.text(cx), "äbcde");
301 assert_eq!(
302 editor.marked_text_ranges(cx),
303 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
304 );
305
306 // Finalize IME composition.
307 editor.replace_text_in_range(None, "ā", window, cx);
308 assert_eq!(editor.text(cx), "ābcde");
309 assert_eq!(editor.marked_text_ranges(cx), None);
310
311 // IME composition edits are grouped and are undone/redone at once.
312 editor.undo(&Default::default(), window, cx);
313 assert_eq!(editor.text(cx), "abcde");
314 assert_eq!(editor.marked_text_ranges(cx), None);
315 editor.redo(&Default::default(), window, cx);
316 assert_eq!(editor.text(cx), "ābcde");
317 assert_eq!(editor.marked_text_ranges(cx), None);
318
319 // Start a new IME composition.
320 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
321 assert_eq!(
322 editor.marked_text_ranges(cx),
323 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
324 );
325
326 // Undoing during an IME composition cancels it.
327 editor.undo(&Default::default(), window, cx);
328 assert_eq!(editor.text(cx), "ābcde");
329 assert_eq!(editor.marked_text_ranges(cx), None);
330
331 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
332 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
333 assert_eq!(editor.text(cx), "ābcdè");
334 assert_eq!(
335 editor.marked_text_ranges(cx),
336 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
337 );
338
339 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
340 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
341 assert_eq!(editor.text(cx), "ābcdę");
342 assert_eq!(editor.marked_text_ranges(cx), None);
343
344 // Start a new IME composition with multiple cursors.
345 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
346 s.select_ranges([
347 OffsetUtf16(1)..OffsetUtf16(1),
348 OffsetUtf16(3)..OffsetUtf16(3),
349 OffsetUtf16(5)..OffsetUtf16(5),
350 ])
351 });
352 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
353 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
354 assert_eq!(
355 editor.marked_text_ranges(cx),
356 Some(vec![
357 OffsetUtf16(0)..OffsetUtf16(3),
358 OffsetUtf16(4)..OffsetUtf16(7),
359 OffsetUtf16(8)..OffsetUtf16(11)
360 ])
361 );
362
363 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
364 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
365 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
366 assert_eq!(
367 editor.marked_text_ranges(cx),
368 Some(vec![
369 OffsetUtf16(1)..OffsetUtf16(2),
370 OffsetUtf16(5)..OffsetUtf16(6),
371 OffsetUtf16(9)..OffsetUtf16(10)
372 ])
373 );
374
375 // Finalize IME composition with multiple cursors.
376 editor.replace_text_in_range(Some(9..10), "2", window, cx);
377 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
378 assert_eq!(editor.marked_text_ranges(cx), None);
379
380 editor
381 });
382}
383
384#[gpui::test]
385fn test_selection_with_mouse(cx: &mut TestAppContext) {
386 init_test(cx, |_| {});
387
388 let editor = cx.add_window(|window, cx| {
389 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
390 build_editor(buffer, window, cx)
391 });
392
393 _ = editor.update(cx, |editor, window, cx| {
394 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
395 });
396 assert_eq!(
397 editor
398 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
399 .unwrap(),
400 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
401 );
402
403 _ = editor.update(cx, |editor, window, cx| {
404 editor.update_selection(
405 DisplayPoint::new(DisplayRow(3), 3),
406 0,
407 gpui::Point::<f32>::default(),
408 window,
409 cx,
410 );
411 });
412
413 assert_eq!(
414 editor
415 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
416 .unwrap(),
417 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
418 );
419
420 _ = editor.update(cx, |editor, window, cx| {
421 editor.update_selection(
422 DisplayPoint::new(DisplayRow(1), 1),
423 0,
424 gpui::Point::<f32>::default(),
425 window,
426 cx,
427 );
428 });
429
430 assert_eq!(
431 editor
432 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
433 .unwrap(),
434 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
435 );
436
437 _ = editor.update(cx, |editor, window, cx| {
438 editor.end_selection(window, cx);
439 editor.update_selection(
440 DisplayPoint::new(DisplayRow(3), 3),
441 0,
442 gpui::Point::<f32>::default(),
443 window,
444 cx,
445 );
446 });
447
448 assert_eq!(
449 editor
450 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
451 .unwrap(),
452 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
453 );
454
455 _ = editor.update(cx, |editor, window, cx| {
456 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
457 editor.update_selection(
458 DisplayPoint::new(DisplayRow(0), 0),
459 0,
460 gpui::Point::<f32>::default(),
461 window,
462 cx,
463 );
464 });
465
466 assert_eq!(
467 editor
468 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
469 .unwrap(),
470 [
471 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
472 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
473 ]
474 );
475
476 _ = editor.update(cx, |editor, window, cx| {
477 editor.end_selection(window, cx);
478 });
479
480 assert_eq!(
481 editor
482 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
483 .unwrap(),
484 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
485 );
486}
487
488#[gpui::test]
489fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
490 init_test(cx, |_| {});
491
492 let editor = cx.add_window(|window, cx| {
493 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
494 build_editor(buffer, window, cx)
495 });
496
497 _ = editor.update(cx, |editor, window, cx| {
498 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
499 });
500
501 _ = editor.update(cx, |editor, window, cx| {
502 editor.end_selection(window, cx);
503 });
504
505 _ = editor.update(cx, |editor, window, cx| {
506 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
507 });
508
509 _ = editor.update(cx, |editor, window, cx| {
510 editor.end_selection(window, cx);
511 });
512
513 assert_eq!(
514 editor
515 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
516 .unwrap(),
517 [
518 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
519 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
520 ]
521 );
522
523 _ = editor.update(cx, |editor, window, cx| {
524 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
525 });
526
527 _ = editor.update(cx, |editor, window, cx| {
528 editor.end_selection(window, cx);
529 });
530
531 assert_eq!(
532 editor
533 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
534 .unwrap(),
535 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
536 );
537}
538
539#[gpui::test]
540fn test_canceling_pending_selection(cx: &mut TestAppContext) {
541 init_test(cx, |_| {});
542
543 let editor = cx.add_window(|window, cx| {
544 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
545 build_editor(buffer, window, cx)
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
550 assert_eq!(
551 editor.selections.display_ranges(cx),
552 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
553 );
554 });
555
556 _ = editor.update(cx, |editor, window, cx| {
557 editor.update_selection(
558 DisplayPoint::new(DisplayRow(3), 3),
559 0,
560 gpui::Point::<f32>::default(),
561 window,
562 cx,
563 );
564 assert_eq!(
565 editor.selections.display_ranges(cx),
566 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
567 );
568 });
569
570 _ = editor.update(cx, |editor, window, cx| {
571 editor.cancel(&Cancel, window, cx);
572 editor.update_selection(
573 DisplayPoint::new(DisplayRow(1), 1),
574 0,
575 gpui::Point::<f32>::default(),
576 window,
577 cx,
578 );
579 assert_eq!(
580 editor.selections.display_ranges(cx),
581 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
582 );
583 });
584}
585
586#[gpui::test]
587fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
588 init_test(cx, |_| {});
589
590 let editor = cx.add_window(|window, cx| {
591 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
592 build_editor(buffer, window, cx)
593 });
594
595 _ = editor.update(cx, |editor, window, cx| {
596 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
597 assert_eq!(
598 editor.selections.display_ranges(cx),
599 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
600 );
601
602 editor.move_down(&Default::default(), window, cx);
603 assert_eq!(
604 editor.selections.display_ranges(cx),
605 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
606 );
607
608 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
609 assert_eq!(
610 editor.selections.display_ranges(cx),
611 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
612 );
613
614 editor.move_up(&Default::default(), window, cx);
615 assert_eq!(
616 editor.selections.display_ranges(cx),
617 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
618 );
619 });
620}
621
622#[gpui::test]
623fn test_extending_selection(cx: &mut TestAppContext) {
624 init_test(cx, |_| {});
625
626 let editor = cx.add_window(|window, cx| {
627 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
628 build_editor(buffer, window, cx)
629 });
630
631 _ = editor.update(cx, |editor, window, cx| {
632 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
633 editor.end_selection(window, cx);
634 assert_eq!(
635 editor.selections.display_ranges(cx),
636 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
637 );
638
639 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
640 editor.end_selection(window, cx);
641 assert_eq!(
642 editor.selections.display_ranges(cx),
643 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
644 );
645
646 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
647 editor.end_selection(window, cx);
648 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
649 assert_eq!(
650 editor.selections.display_ranges(cx),
651 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
652 );
653
654 editor.update_selection(
655 DisplayPoint::new(DisplayRow(0), 1),
656 0,
657 gpui::Point::<f32>::default(),
658 window,
659 cx,
660 );
661 editor.end_selection(window, cx);
662 assert_eq!(
663 editor.selections.display_ranges(cx),
664 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
665 );
666
667 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
668 editor.end_selection(window, cx);
669 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
670 editor.end_selection(window, cx);
671 assert_eq!(
672 editor.selections.display_ranges(cx),
673 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
674 );
675
676 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
677 assert_eq!(
678 editor.selections.display_ranges(cx),
679 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
680 );
681
682 editor.update_selection(
683 DisplayPoint::new(DisplayRow(0), 6),
684 0,
685 gpui::Point::<f32>::default(),
686 window,
687 cx,
688 );
689 assert_eq!(
690 editor.selections.display_ranges(cx),
691 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
692 );
693
694 editor.update_selection(
695 DisplayPoint::new(DisplayRow(0), 1),
696 0,
697 gpui::Point::<f32>::default(),
698 window,
699 cx,
700 );
701 editor.end_selection(window, cx);
702 assert_eq!(
703 editor.selections.display_ranges(cx),
704 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
705 );
706 });
707}
708
709#[gpui::test]
710fn test_clone(cx: &mut TestAppContext) {
711 init_test(cx, |_| {});
712
713 let (text, selection_ranges) = marked_text_ranges(
714 indoc! {"
715 one
716 two
717 threeˇ
718 four
719 fiveˇ
720 "},
721 true,
722 );
723
724 let editor = cx.add_window(|window, cx| {
725 let buffer = MultiBuffer::build_simple(&text, cx);
726 build_editor(buffer, window, cx)
727 });
728
729 _ = editor.update(cx, |editor, window, cx| {
730 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
731 s.select_ranges(selection_ranges.clone())
732 });
733 editor.fold_creases(
734 vec![
735 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
736 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
737 ],
738 true,
739 window,
740 cx,
741 );
742 });
743
744 let cloned_editor = editor
745 .update(cx, |editor, _, cx| {
746 cx.open_window(Default::default(), |window, cx| {
747 cx.new(|cx| editor.clone(window, cx))
748 })
749 })
750 .unwrap()
751 .unwrap();
752
753 let snapshot = editor
754 .update(cx, |e, window, cx| e.snapshot(window, cx))
755 .unwrap();
756 let cloned_snapshot = cloned_editor
757 .update(cx, |e, window, cx| e.snapshot(window, cx))
758 .unwrap();
759
760 assert_eq!(
761 cloned_editor
762 .update(cx, |e, _, cx| e.display_text(cx))
763 .unwrap(),
764 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
765 );
766 assert_eq!(
767 cloned_snapshot
768 .folds_in_range(0..text.len())
769 .collect::<Vec<_>>(),
770 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
771 );
772 assert_set_eq!(
773 cloned_editor
774 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
775 .unwrap(),
776 editor
777 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
778 .unwrap()
779 );
780 assert_set_eq!(
781 cloned_editor
782 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
783 .unwrap(),
784 editor
785 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
786 .unwrap()
787 );
788}
789
790#[gpui::test]
791async fn test_navigation_history(cx: &mut TestAppContext) {
792 init_test(cx, |_| {});
793
794 use workspace::item::Item;
795
796 let fs = FakeFs::new(cx.executor());
797 let project = Project::test(fs, [], cx).await;
798 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
799 let pane = workspace
800 .update(cx, |workspace, _, _| workspace.active_pane().clone())
801 .unwrap();
802
803 _ = workspace.update(cx, |_v, window, cx| {
804 cx.new(|cx| {
805 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
806 let mut editor = build_editor(buffer, window, cx);
807 let handle = cx.entity();
808 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
809
810 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
811 editor.nav_history.as_mut().unwrap().pop_backward(cx)
812 }
813
814 // Move the cursor a small distance.
815 // Nothing is added to the navigation history.
816 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
817 s.select_display_ranges([
818 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
819 ])
820 });
821 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
822 s.select_display_ranges([
823 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
824 ])
825 });
826 assert!(pop_history(&mut editor, cx).is_none());
827
828 // Move the cursor a large distance.
829 // The history can jump back to the previous position.
830 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
831 s.select_display_ranges([
832 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
833 ])
834 });
835 let nav_entry = pop_history(&mut editor, cx).unwrap();
836 editor.navigate(nav_entry.data.unwrap(), window, cx);
837 assert_eq!(nav_entry.item.id(), cx.entity_id());
838 assert_eq!(
839 editor.selections.display_ranges(cx),
840 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
841 );
842 assert!(pop_history(&mut editor, cx).is_none());
843
844 // Move the cursor a small distance via the mouse.
845 // Nothing is added to the navigation history.
846 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
847 editor.end_selection(window, cx);
848 assert_eq!(
849 editor.selections.display_ranges(cx),
850 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
851 );
852 assert!(pop_history(&mut editor, cx).is_none());
853
854 // Move the cursor a large distance via the mouse.
855 // The history can jump back to the previous position.
856 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
857 editor.end_selection(window, cx);
858 assert_eq!(
859 editor.selections.display_ranges(cx),
860 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
861 );
862 let nav_entry = pop_history(&mut editor, cx).unwrap();
863 editor.navigate(nav_entry.data.unwrap(), window, cx);
864 assert_eq!(nav_entry.item.id(), cx.entity_id());
865 assert_eq!(
866 editor.selections.display_ranges(cx),
867 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
868 );
869 assert!(pop_history(&mut editor, cx).is_none());
870
871 // Set scroll position to check later
872 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
873 let original_scroll_position = editor.scroll_manager.anchor();
874
875 // Jump to the end of the document and adjust scroll
876 editor.move_to_end(&MoveToEnd, window, cx);
877 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
878 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
879
880 let nav_entry = pop_history(&mut editor, cx).unwrap();
881 editor.navigate(nav_entry.data.unwrap(), window, cx);
882 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
883
884 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
885 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
886 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
887 let invalid_point = Point::new(9999, 0);
888 editor.navigate(
889 Box::new(NavigationData {
890 cursor_anchor: invalid_anchor,
891 cursor_position: invalid_point,
892 scroll_anchor: ScrollAnchor {
893 anchor: invalid_anchor,
894 offset: Default::default(),
895 },
896 scroll_top_row: invalid_point.row,
897 }),
898 window,
899 cx,
900 );
901 assert_eq!(
902 editor.selections.display_ranges(cx),
903 &[editor.max_point(cx)..editor.max_point(cx)]
904 );
905 assert_eq!(
906 editor.scroll_position(cx),
907 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
908 );
909
910 editor
911 })
912 });
913}
914
915#[gpui::test]
916fn test_cancel(cx: &mut TestAppContext) {
917 init_test(cx, |_| {});
918
919 let editor = cx.add_window(|window, cx| {
920 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
921 build_editor(buffer, window, cx)
922 });
923
924 _ = editor.update(cx, |editor, window, cx| {
925 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
926 editor.update_selection(
927 DisplayPoint::new(DisplayRow(1), 1),
928 0,
929 gpui::Point::<f32>::default(),
930 window,
931 cx,
932 );
933 editor.end_selection(window, cx);
934
935 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
936 editor.update_selection(
937 DisplayPoint::new(DisplayRow(0), 3),
938 0,
939 gpui::Point::<f32>::default(),
940 window,
941 cx,
942 );
943 editor.end_selection(window, cx);
944 assert_eq!(
945 editor.selections.display_ranges(cx),
946 [
947 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
948 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
949 ]
950 );
951 });
952
953 _ = editor.update(cx, |editor, window, cx| {
954 editor.cancel(&Cancel, window, cx);
955 assert_eq!(
956 editor.selections.display_ranges(cx),
957 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
958 );
959 });
960
961 _ = editor.update(cx, |editor, window, cx| {
962 editor.cancel(&Cancel, window, cx);
963 assert_eq!(
964 editor.selections.display_ranges(cx),
965 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
966 );
967 });
968}
969
970#[gpui::test]
971fn test_fold_action(cx: &mut TestAppContext) {
972 init_test(cx, |_| {});
973
974 let editor = cx.add_window(|window, cx| {
975 let buffer = MultiBuffer::build_simple(
976 &"
977 impl Foo {
978 // Hello!
979
980 fn a() {
981 1
982 }
983
984 fn b() {
985 2
986 }
987
988 fn c() {
989 3
990 }
991 }
992 "
993 .unindent(),
994 cx,
995 );
996 build_editor(buffer, window, cx)
997 });
998
999 _ = editor.update(cx, |editor, window, cx| {
1000 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1001 s.select_display_ranges([
1002 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1003 ]);
1004 });
1005 editor.fold(&Fold, window, cx);
1006 assert_eq!(
1007 editor.display_text(cx),
1008 "
1009 impl Foo {
1010 // Hello!
1011
1012 fn a() {
1013 1
1014 }
1015
1016 fn b() {⋯
1017 }
1018
1019 fn c() {⋯
1020 }
1021 }
1022 "
1023 .unindent(),
1024 );
1025
1026 editor.fold(&Fold, window, cx);
1027 assert_eq!(
1028 editor.display_text(cx),
1029 "
1030 impl Foo {⋯
1031 }
1032 "
1033 .unindent(),
1034 );
1035
1036 editor.unfold_lines(&UnfoldLines, window, cx);
1037 assert_eq!(
1038 editor.display_text(cx),
1039 "
1040 impl Foo {
1041 // Hello!
1042
1043 fn a() {
1044 1
1045 }
1046
1047 fn b() {⋯
1048 }
1049
1050 fn c() {⋯
1051 }
1052 }
1053 "
1054 .unindent(),
1055 );
1056
1057 editor.unfold_lines(&UnfoldLines, window, cx);
1058 assert_eq!(
1059 editor.display_text(cx),
1060 editor.buffer.read(cx).read(cx).text()
1061 );
1062 });
1063}
1064
1065#[gpui::test]
1066fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1067 init_test(cx, |_| {});
1068
1069 let editor = cx.add_window(|window, cx| {
1070 let buffer = MultiBuffer::build_simple(
1071 &"
1072 class Foo:
1073 # Hello!
1074
1075 def a():
1076 print(1)
1077
1078 def b():
1079 print(2)
1080
1081 def c():
1082 print(3)
1083 "
1084 .unindent(),
1085 cx,
1086 );
1087 build_editor(buffer, window, cx)
1088 });
1089
1090 _ = editor.update(cx, |editor, window, cx| {
1091 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1092 s.select_display_ranges([
1093 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1094 ]);
1095 });
1096 editor.fold(&Fold, window, cx);
1097 assert_eq!(
1098 editor.display_text(cx),
1099 "
1100 class Foo:
1101 # Hello!
1102
1103 def a():
1104 print(1)
1105
1106 def b():⋯
1107
1108 def c():⋯
1109 "
1110 .unindent(),
1111 );
1112
1113 editor.fold(&Fold, window, cx);
1114 assert_eq!(
1115 editor.display_text(cx),
1116 "
1117 class Foo:⋯
1118 "
1119 .unindent(),
1120 );
1121
1122 editor.unfold_lines(&UnfoldLines, window, cx);
1123 assert_eq!(
1124 editor.display_text(cx),
1125 "
1126 class Foo:
1127 # Hello!
1128
1129 def a():
1130 print(1)
1131
1132 def b():⋯
1133
1134 def c():⋯
1135 "
1136 .unindent(),
1137 );
1138
1139 editor.unfold_lines(&UnfoldLines, window, cx);
1140 assert_eq!(
1141 editor.display_text(cx),
1142 editor.buffer.read(cx).read(cx).text()
1143 );
1144 });
1145}
1146
1147#[gpui::test]
1148fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1149 init_test(cx, |_| {});
1150
1151 let editor = cx.add_window(|window, cx| {
1152 let buffer = MultiBuffer::build_simple(
1153 &"
1154 class Foo:
1155 # Hello!
1156
1157 def a():
1158 print(1)
1159
1160 def b():
1161 print(2)
1162
1163
1164 def c():
1165 print(3)
1166
1167
1168 "
1169 .unindent(),
1170 cx,
1171 );
1172 build_editor(buffer, window, cx)
1173 });
1174
1175 _ = editor.update(cx, |editor, window, cx| {
1176 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1177 s.select_display_ranges([
1178 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1179 ]);
1180 });
1181 editor.fold(&Fold, window, cx);
1182 assert_eq!(
1183 editor.display_text(cx),
1184 "
1185 class Foo:
1186 # Hello!
1187
1188 def a():
1189 print(1)
1190
1191 def b():⋯
1192
1193
1194 def c():⋯
1195
1196
1197 "
1198 .unindent(),
1199 );
1200
1201 editor.fold(&Fold, window, cx);
1202 assert_eq!(
1203 editor.display_text(cx),
1204 "
1205 class Foo:⋯
1206
1207
1208 "
1209 .unindent(),
1210 );
1211
1212 editor.unfold_lines(&UnfoldLines, window, cx);
1213 assert_eq!(
1214 editor.display_text(cx),
1215 "
1216 class Foo:
1217 # Hello!
1218
1219 def a():
1220 print(1)
1221
1222 def b():⋯
1223
1224
1225 def c():⋯
1226
1227
1228 "
1229 .unindent(),
1230 );
1231
1232 editor.unfold_lines(&UnfoldLines, window, cx);
1233 assert_eq!(
1234 editor.display_text(cx),
1235 editor.buffer.read(cx).read(cx).text()
1236 );
1237 });
1238}
1239
1240#[gpui::test]
1241fn test_fold_at_level(cx: &mut TestAppContext) {
1242 init_test(cx, |_| {});
1243
1244 let editor = cx.add_window(|window, cx| {
1245 let buffer = MultiBuffer::build_simple(
1246 &"
1247 class Foo:
1248 # Hello!
1249
1250 def a():
1251 print(1)
1252
1253 def b():
1254 print(2)
1255
1256
1257 class Bar:
1258 # World!
1259
1260 def a():
1261 print(1)
1262
1263 def b():
1264 print(2)
1265
1266
1267 "
1268 .unindent(),
1269 cx,
1270 );
1271 build_editor(buffer, window, cx)
1272 });
1273
1274 _ = editor.update(cx, |editor, window, cx| {
1275 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1276 assert_eq!(
1277 editor.display_text(cx),
1278 "
1279 class Foo:
1280 # Hello!
1281
1282 def a():⋯
1283
1284 def b():⋯
1285
1286
1287 class Bar:
1288 # World!
1289
1290 def a():⋯
1291
1292 def b():⋯
1293
1294
1295 "
1296 .unindent(),
1297 );
1298
1299 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1300 assert_eq!(
1301 editor.display_text(cx),
1302 "
1303 class Foo:⋯
1304
1305
1306 class Bar:⋯
1307
1308
1309 "
1310 .unindent(),
1311 );
1312
1313 editor.unfold_all(&UnfoldAll, window, cx);
1314 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1315 assert_eq!(
1316 editor.display_text(cx),
1317 "
1318 class Foo:
1319 # Hello!
1320
1321 def a():
1322 print(1)
1323
1324 def b():
1325 print(2)
1326
1327
1328 class Bar:
1329 # World!
1330
1331 def a():
1332 print(1)
1333
1334 def b():
1335 print(2)
1336
1337
1338 "
1339 .unindent(),
1340 );
1341
1342 assert_eq!(
1343 editor.display_text(cx),
1344 editor.buffer.read(cx).read(cx).text()
1345 );
1346 let (_, positions) = marked_text_ranges(
1347 &"
1348 class Foo:
1349 # Hello!
1350
1351 def a():
1352 print(1)
1353
1354 def b():
1355 p«riˇ»nt(2)
1356
1357
1358 class Bar:
1359 # World!
1360
1361 def a():
1362 «ˇprint(1)
1363
1364 def b():
1365 print(2)»
1366
1367
1368 "
1369 .unindent(),
1370 true,
1371 );
1372
1373 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1374 s.select_ranges(positions)
1375 });
1376
1377 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1378 assert_eq!(
1379 editor.display_text(cx),
1380 "
1381 class Foo:
1382 # Hello!
1383
1384 def a():⋯
1385
1386 def b():
1387 print(2)
1388
1389
1390 class Bar:
1391 # World!
1392
1393 def a():
1394 print(1)
1395
1396 def b():
1397 print(2)
1398
1399
1400 "
1401 .unindent(),
1402 );
1403 });
1404}
1405
1406#[gpui::test]
1407fn test_move_cursor(cx: &mut TestAppContext) {
1408 init_test(cx, |_| {});
1409
1410 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1411 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1412
1413 buffer.update(cx, |buffer, cx| {
1414 buffer.edit(
1415 vec![
1416 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1417 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1418 ],
1419 None,
1420 cx,
1421 );
1422 });
1423 _ = editor.update(cx, |editor, window, cx| {
1424 assert_eq!(
1425 editor.selections.display_ranges(cx),
1426 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1427 );
1428
1429 editor.move_down(&MoveDown, window, cx);
1430 assert_eq!(
1431 editor.selections.display_ranges(cx),
1432 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1433 );
1434
1435 editor.move_right(&MoveRight, window, cx);
1436 assert_eq!(
1437 editor.selections.display_ranges(cx),
1438 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1439 );
1440
1441 editor.move_left(&MoveLeft, window, cx);
1442 assert_eq!(
1443 editor.selections.display_ranges(cx),
1444 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1445 );
1446
1447 editor.move_up(&MoveUp, window, cx);
1448 assert_eq!(
1449 editor.selections.display_ranges(cx),
1450 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1451 );
1452
1453 editor.move_to_end(&MoveToEnd, window, cx);
1454 assert_eq!(
1455 editor.selections.display_ranges(cx),
1456 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1457 );
1458
1459 editor.move_to_beginning(&MoveToBeginning, window, cx);
1460 assert_eq!(
1461 editor.selections.display_ranges(cx),
1462 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1463 );
1464
1465 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1466 s.select_display_ranges([
1467 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1468 ]);
1469 });
1470 editor.select_to_beginning(&SelectToBeginning, window, cx);
1471 assert_eq!(
1472 editor.selections.display_ranges(cx),
1473 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1474 );
1475
1476 editor.select_to_end(&SelectToEnd, window, cx);
1477 assert_eq!(
1478 editor.selections.display_ranges(cx),
1479 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1480 );
1481 });
1482}
1483
1484#[gpui::test]
1485fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1486 init_test(cx, |_| {});
1487
1488 let editor = cx.add_window(|window, cx| {
1489 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1490 build_editor(buffer, window, cx)
1491 });
1492
1493 assert_eq!('🟥'.len_utf8(), 4);
1494 assert_eq!('α'.len_utf8(), 2);
1495
1496 _ = editor.update(cx, |editor, window, cx| {
1497 editor.fold_creases(
1498 vec![
1499 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1500 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1501 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1502 ],
1503 true,
1504 window,
1505 cx,
1506 );
1507 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1508
1509 editor.move_right(&MoveRight, window, cx);
1510 assert_eq!(
1511 editor.selections.display_ranges(cx),
1512 &[empty_range(0, "🟥".len())]
1513 );
1514 editor.move_right(&MoveRight, window, cx);
1515 assert_eq!(
1516 editor.selections.display_ranges(cx),
1517 &[empty_range(0, "🟥🟧".len())]
1518 );
1519 editor.move_right(&MoveRight, window, cx);
1520 assert_eq!(
1521 editor.selections.display_ranges(cx),
1522 &[empty_range(0, "🟥🟧⋯".len())]
1523 );
1524
1525 editor.move_down(&MoveDown, window, cx);
1526 assert_eq!(
1527 editor.selections.display_ranges(cx),
1528 &[empty_range(1, "ab⋯e".len())]
1529 );
1530 editor.move_left(&MoveLeft, window, cx);
1531 assert_eq!(
1532 editor.selections.display_ranges(cx),
1533 &[empty_range(1, "ab⋯".len())]
1534 );
1535 editor.move_left(&MoveLeft, window, cx);
1536 assert_eq!(
1537 editor.selections.display_ranges(cx),
1538 &[empty_range(1, "ab".len())]
1539 );
1540 editor.move_left(&MoveLeft, window, cx);
1541 assert_eq!(
1542 editor.selections.display_ranges(cx),
1543 &[empty_range(1, "a".len())]
1544 );
1545
1546 editor.move_down(&MoveDown, window, cx);
1547 assert_eq!(
1548 editor.selections.display_ranges(cx),
1549 &[empty_range(2, "α".len())]
1550 );
1551 editor.move_right(&MoveRight, window, cx);
1552 assert_eq!(
1553 editor.selections.display_ranges(cx),
1554 &[empty_range(2, "αβ".len())]
1555 );
1556 editor.move_right(&MoveRight, window, cx);
1557 assert_eq!(
1558 editor.selections.display_ranges(cx),
1559 &[empty_range(2, "αβ⋯".len())]
1560 );
1561 editor.move_right(&MoveRight, window, cx);
1562 assert_eq!(
1563 editor.selections.display_ranges(cx),
1564 &[empty_range(2, "αβ⋯ε".len())]
1565 );
1566
1567 editor.move_up(&MoveUp, window, cx);
1568 assert_eq!(
1569 editor.selections.display_ranges(cx),
1570 &[empty_range(1, "ab⋯e".len())]
1571 );
1572 editor.move_down(&MoveDown, window, cx);
1573 assert_eq!(
1574 editor.selections.display_ranges(cx),
1575 &[empty_range(2, "αβ⋯ε".len())]
1576 );
1577 editor.move_up(&MoveUp, window, cx);
1578 assert_eq!(
1579 editor.selections.display_ranges(cx),
1580 &[empty_range(1, "ab⋯e".len())]
1581 );
1582
1583 editor.move_up(&MoveUp, window, cx);
1584 assert_eq!(
1585 editor.selections.display_ranges(cx),
1586 &[empty_range(0, "🟥🟧".len())]
1587 );
1588 editor.move_left(&MoveLeft, window, cx);
1589 assert_eq!(
1590 editor.selections.display_ranges(cx),
1591 &[empty_range(0, "🟥".len())]
1592 );
1593 editor.move_left(&MoveLeft, window, cx);
1594 assert_eq!(
1595 editor.selections.display_ranges(cx),
1596 &[empty_range(0, "".len())]
1597 );
1598 });
1599}
1600
1601#[gpui::test]
1602fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1603 init_test(cx, |_| {});
1604
1605 let editor = cx.add_window(|window, cx| {
1606 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1607 build_editor(buffer, window, cx)
1608 });
1609 _ = editor.update(cx, |editor, window, cx| {
1610 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1611 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1612 });
1613
1614 // moving above start of document should move selection to start of document,
1615 // but the next move down should still be at the original goal_x
1616 editor.move_up(&MoveUp, window, cx);
1617 assert_eq!(
1618 editor.selections.display_ranges(cx),
1619 &[empty_range(0, "".len())]
1620 );
1621
1622 editor.move_down(&MoveDown, window, cx);
1623 assert_eq!(
1624 editor.selections.display_ranges(cx),
1625 &[empty_range(1, "abcd".len())]
1626 );
1627
1628 editor.move_down(&MoveDown, window, cx);
1629 assert_eq!(
1630 editor.selections.display_ranges(cx),
1631 &[empty_range(2, "αβγ".len())]
1632 );
1633
1634 editor.move_down(&MoveDown, window, cx);
1635 assert_eq!(
1636 editor.selections.display_ranges(cx),
1637 &[empty_range(3, "abcd".len())]
1638 );
1639
1640 editor.move_down(&MoveDown, window, cx);
1641 assert_eq!(
1642 editor.selections.display_ranges(cx),
1643 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1644 );
1645
1646 // moving past end of document should not change goal_x
1647 editor.move_down(&MoveDown, window, cx);
1648 assert_eq!(
1649 editor.selections.display_ranges(cx),
1650 &[empty_range(5, "".len())]
1651 );
1652
1653 editor.move_down(&MoveDown, window, cx);
1654 assert_eq!(
1655 editor.selections.display_ranges(cx),
1656 &[empty_range(5, "".len())]
1657 );
1658
1659 editor.move_up(&MoveUp, window, cx);
1660 assert_eq!(
1661 editor.selections.display_ranges(cx),
1662 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1663 );
1664
1665 editor.move_up(&MoveUp, window, cx);
1666 assert_eq!(
1667 editor.selections.display_ranges(cx),
1668 &[empty_range(3, "abcd".len())]
1669 );
1670
1671 editor.move_up(&MoveUp, window, cx);
1672 assert_eq!(
1673 editor.selections.display_ranges(cx),
1674 &[empty_range(2, "αβγ".len())]
1675 );
1676 });
1677}
1678
1679#[gpui::test]
1680fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1681 init_test(cx, |_| {});
1682 let move_to_beg = MoveToBeginningOfLine {
1683 stop_at_soft_wraps: true,
1684 stop_at_indent: true,
1685 };
1686
1687 let delete_to_beg = DeleteToBeginningOfLine {
1688 stop_at_indent: false,
1689 };
1690
1691 let move_to_end = MoveToEndOfLine {
1692 stop_at_soft_wraps: true,
1693 };
1694
1695 let editor = cx.add_window(|window, cx| {
1696 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1697 build_editor(buffer, window, cx)
1698 });
1699 _ = editor.update(cx, |editor, window, cx| {
1700 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1701 s.select_display_ranges([
1702 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1703 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1704 ]);
1705 });
1706 });
1707
1708 _ = editor.update(cx, |editor, window, cx| {
1709 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1710 assert_eq!(
1711 editor.selections.display_ranges(cx),
1712 &[
1713 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1714 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1715 ]
1716 );
1717 });
1718
1719 _ = editor.update(cx, |editor, window, cx| {
1720 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1721 assert_eq!(
1722 editor.selections.display_ranges(cx),
1723 &[
1724 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1725 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1726 ]
1727 );
1728 });
1729
1730 _ = editor.update(cx, |editor, window, cx| {
1731 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1732 assert_eq!(
1733 editor.selections.display_ranges(cx),
1734 &[
1735 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1736 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1737 ]
1738 );
1739 });
1740
1741 _ = editor.update(cx, |editor, window, cx| {
1742 editor.move_to_end_of_line(&move_to_end, window, cx);
1743 assert_eq!(
1744 editor.selections.display_ranges(cx),
1745 &[
1746 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1747 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1748 ]
1749 );
1750 });
1751
1752 // Moving to the end of line again is a no-op.
1753 _ = editor.update(cx, |editor, window, cx| {
1754 editor.move_to_end_of_line(&move_to_end, window, cx);
1755 assert_eq!(
1756 editor.selections.display_ranges(cx),
1757 &[
1758 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1759 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1760 ]
1761 );
1762 });
1763
1764 _ = editor.update(cx, |editor, window, cx| {
1765 editor.move_left(&MoveLeft, window, cx);
1766 editor.select_to_beginning_of_line(
1767 &SelectToBeginningOfLine {
1768 stop_at_soft_wraps: true,
1769 stop_at_indent: true,
1770 },
1771 window,
1772 cx,
1773 );
1774 assert_eq!(
1775 editor.selections.display_ranges(cx),
1776 &[
1777 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1778 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1779 ]
1780 );
1781 });
1782
1783 _ = editor.update(cx, |editor, window, cx| {
1784 editor.select_to_beginning_of_line(
1785 &SelectToBeginningOfLine {
1786 stop_at_soft_wraps: true,
1787 stop_at_indent: true,
1788 },
1789 window,
1790 cx,
1791 );
1792 assert_eq!(
1793 editor.selections.display_ranges(cx),
1794 &[
1795 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1796 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1797 ]
1798 );
1799 });
1800
1801 _ = editor.update(cx, |editor, window, cx| {
1802 editor.select_to_beginning_of_line(
1803 &SelectToBeginningOfLine {
1804 stop_at_soft_wraps: true,
1805 stop_at_indent: true,
1806 },
1807 window,
1808 cx,
1809 );
1810 assert_eq!(
1811 editor.selections.display_ranges(cx),
1812 &[
1813 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1814 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1815 ]
1816 );
1817 });
1818
1819 _ = editor.update(cx, |editor, window, cx| {
1820 editor.select_to_end_of_line(
1821 &SelectToEndOfLine {
1822 stop_at_soft_wraps: true,
1823 },
1824 window,
1825 cx,
1826 );
1827 assert_eq!(
1828 editor.selections.display_ranges(cx),
1829 &[
1830 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1831 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1832 ]
1833 );
1834 });
1835
1836 _ = editor.update(cx, |editor, window, cx| {
1837 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1838 assert_eq!(editor.display_text(cx), "ab\n de");
1839 assert_eq!(
1840 editor.selections.display_ranges(cx),
1841 &[
1842 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1843 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1844 ]
1845 );
1846 });
1847
1848 _ = editor.update(cx, |editor, window, cx| {
1849 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1850 assert_eq!(editor.display_text(cx), "\n");
1851 assert_eq!(
1852 editor.selections.display_ranges(cx),
1853 &[
1854 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1855 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1856 ]
1857 );
1858 });
1859}
1860
1861#[gpui::test]
1862fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1863 init_test(cx, |_| {});
1864 let move_to_beg = MoveToBeginningOfLine {
1865 stop_at_soft_wraps: false,
1866 stop_at_indent: false,
1867 };
1868
1869 let move_to_end = MoveToEndOfLine {
1870 stop_at_soft_wraps: false,
1871 };
1872
1873 let editor = cx.add_window(|window, cx| {
1874 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1875 build_editor(buffer, window, cx)
1876 });
1877
1878 _ = editor.update(cx, |editor, window, cx| {
1879 editor.set_wrap_width(Some(140.0.into()), cx);
1880
1881 // We expect the following lines after wrapping
1882 // ```
1883 // thequickbrownfox
1884 // jumpedoverthelazydo
1885 // gs
1886 // ```
1887 // The final `gs` was soft-wrapped onto a new line.
1888 assert_eq!(
1889 "thequickbrownfox\njumpedoverthelaz\nydogs",
1890 editor.display_text(cx),
1891 );
1892
1893 // First, let's assert behavior on the first line, that was not soft-wrapped.
1894 // Start the cursor at the `k` on the first line
1895 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1896 s.select_display_ranges([
1897 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1898 ]);
1899 });
1900
1901 // Moving to the beginning of the line should put us at the beginning of the line.
1902 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1903 assert_eq!(
1904 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1905 editor.selections.display_ranges(cx)
1906 );
1907
1908 // Moving to the end of the line should put us at the end of the line.
1909 editor.move_to_end_of_line(&move_to_end, window, cx);
1910 assert_eq!(
1911 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1912 editor.selections.display_ranges(cx)
1913 );
1914
1915 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1916 // Start the cursor at the last line (`y` that was wrapped to a new line)
1917 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1918 s.select_display_ranges([
1919 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1920 ]);
1921 });
1922
1923 // Moving to the beginning of the line should put us at the start of the second line of
1924 // display text, i.e., the `j`.
1925 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1926 assert_eq!(
1927 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1928 editor.selections.display_ranges(cx)
1929 );
1930
1931 // Moving to the beginning of the line again should be a no-op.
1932 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1933 assert_eq!(
1934 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1935 editor.selections.display_ranges(cx)
1936 );
1937
1938 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1939 // next display line.
1940 editor.move_to_end_of_line(&move_to_end, window, cx);
1941 assert_eq!(
1942 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1943 editor.selections.display_ranges(cx)
1944 );
1945
1946 // Moving to the end of the line again should be a no-op.
1947 editor.move_to_end_of_line(&move_to_end, window, cx);
1948 assert_eq!(
1949 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1950 editor.selections.display_ranges(cx)
1951 );
1952 });
1953}
1954
1955#[gpui::test]
1956fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1957 init_test(cx, |_| {});
1958
1959 let move_to_beg = MoveToBeginningOfLine {
1960 stop_at_soft_wraps: true,
1961 stop_at_indent: true,
1962 };
1963
1964 let select_to_beg = SelectToBeginningOfLine {
1965 stop_at_soft_wraps: true,
1966 stop_at_indent: true,
1967 };
1968
1969 let delete_to_beg = DeleteToBeginningOfLine {
1970 stop_at_indent: true,
1971 };
1972
1973 let move_to_end = MoveToEndOfLine {
1974 stop_at_soft_wraps: false,
1975 };
1976
1977 let editor = cx.add_window(|window, cx| {
1978 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1979 build_editor(buffer, window, cx)
1980 });
1981
1982 _ = editor.update(cx, |editor, window, cx| {
1983 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1984 s.select_display_ranges([
1985 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1986 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1987 ]);
1988 });
1989
1990 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1991 // and the second cursor at the first non-whitespace character in the line.
1992 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1993 assert_eq!(
1994 editor.selections.display_ranges(cx),
1995 &[
1996 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1997 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1998 ]
1999 );
2000
2001 // Moving to the beginning of the line again should be a no-op for the first cursor,
2002 // and should move the second cursor to the beginning of the line.
2003 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2004 assert_eq!(
2005 editor.selections.display_ranges(cx),
2006 &[
2007 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2008 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
2009 ]
2010 );
2011
2012 // Moving to the beginning of the line again should still be a no-op for the first cursor,
2013 // and should move the second cursor back to the first non-whitespace character in the line.
2014 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2015 assert_eq!(
2016 editor.selections.display_ranges(cx),
2017 &[
2018 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2019 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2020 ]
2021 );
2022
2023 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
2024 // and to the first non-whitespace character in the line for the second cursor.
2025 editor.move_to_end_of_line(&move_to_end, window, cx);
2026 editor.move_left(&MoveLeft, window, cx);
2027 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2028 assert_eq!(
2029 editor.selections.display_ranges(cx),
2030 &[
2031 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2032 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2033 ]
2034 );
2035
2036 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2037 // and should select to the beginning of the line for the second cursor.
2038 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2039 assert_eq!(
2040 editor.selections.display_ranges(cx),
2041 &[
2042 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2043 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2044 ]
2045 );
2046
2047 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2048 // and should delete to the first non-whitespace character in the line for the second cursor.
2049 editor.move_to_end_of_line(&move_to_end, window, cx);
2050 editor.move_left(&MoveLeft, window, cx);
2051 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2052 assert_eq!(editor.text(cx), "c\n f");
2053 });
2054}
2055
2056#[gpui::test]
2057fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2058 init_test(cx, |_| {});
2059
2060 let move_to_beg = MoveToBeginningOfLine {
2061 stop_at_soft_wraps: true,
2062 stop_at_indent: true,
2063 };
2064
2065 let editor = cx.add_window(|window, cx| {
2066 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2067 build_editor(buffer, window, cx)
2068 });
2069
2070 _ = editor.update(cx, |editor, window, cx| {
2071 // test cursor between line_start and indent_start
2072 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2073 s.select_display_ranges([
2074 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2075 ]);
2076 });
2077
2078 // cursor should move to line_start
2079 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2080 assert_eq!(
2081 editor.selections.display_ranges(cx),
2082 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2083 );
2084
2085 // cursor should move to indent_start
2086 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2087 assert_eq!(
2088 editor.selections.display_ranges(cx),
2089 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2090 );
2091
2092 // cursor should move to back to line_start
2093 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2094 assert_eq!(
2095 editor.selections.display_ranges(cx),
2096 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2097 );
2098 });
2099}
2100
2101#[gpui::test]
2102fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2103 init_test(cx, |_| {});
2104
2105 let editor = cx.add_window(|window, cx| {
2106 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2107 build_editor(buffer, window, cx)
2108 });
2109 _ = editor.update(cx, |editor, window, cx| {
2110 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2111 s.select_display_ranges([
2112 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2113 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2114 ])
2115 });
2116 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2117 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2118
2119 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2120 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2121
2122 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2123 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2124
2125 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2126 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2127
2128 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2129 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2130
2131 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2132 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2133
2134 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2135 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2136
2137 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2138 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2139
2140 editor.move_right(&MoveRight, window, cx);
2141 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2142 assert_selection_ranges(
2143 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2144 editor,
2145 cx,
2146 );
2147
2148 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2149 assert_selection_ranges(
2150 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2151 editor,
2152 cx,
2153 );
2154
2155 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2156 assert_selection_ranges(
2157 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2158 editor,
2159 cx,
2160 );
2161 });
2162}
2163
2164#[gpui::test]
2165fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2166 init_test(cx, |_| {});
2167
2168 let editor = cx.add_window(|window, cx| {
2169 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2170 build_editor(buffer, window, cx)
2171 });
2172
2173 _ = editor.update(cx, |editor, window, cx| {
2174 editor.set_wrap_width(Some(140.0.into()), cx);
2175 assert_eq!(
2176 editor.display_text(cx),
2177 "use one::{\n two::three::\n four::five\n};"
2178 );
2179
2180 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2181 s.select_display_ranges([
2182 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2183 ]);
2184 });
2185
2186 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2187 assert_eq!(
2188 editor.selections.display_ranges(cx),
2189 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2190 );
2191
2192 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2193 assert_eq!(
2194 editor.selections.display_ranges(cx),
2195 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2196 );
2197
2198 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2199 assert_eq!(
2200 editor.selections.display_ranges(cx),
2201 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2202 );
2203
2204 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2205 assert_eq!(
2206 editor.selections.display_ranges(cx),
2207 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2208 );
2209
2210 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2211 assert_eq!(
2212 editor.selections.display_ranges(cx),
2213 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2214 );
2215
2216 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2217 assert_eq!(
2218 editor.selections.display_ranges(cx),
2219 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2220 );
2221 });
2222}
2223
2224#[gpui::test]
2225async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2226 init_test(cx, |_| {});
2227 let mut cx = EditorTestContext::new(cx).await;
2228
2229 let line_height = cx.editor(|editor, window, _| {
2230 editor
2231 .style()
2232 .unwrap()
2233 .text
2234 .line_height_in_pixels(window.rem_size())
2235 });
2236 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2237
2238 cx.set_state(
2239 &r#"ˇone
2240 two
2241
2242 three
2243 fourˇ
2244 five
2245
2246 six"#
2247 .unindent(),
2248 );
2249
2250 cx.update_editor(|editor, window, cx| {
2251 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2252 });
2253 cx.assert_editor_state(
2254 &r#"one
2255 two
2256 ˇ
2257 three
2258 four
2259 five
2260 ˇ
2261 six"#
2262 .unindent(),
2263 );
2264
2265 cx.update_editor(|editor, window, cx| {
2266 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2267 });
2268 cx.assert_editor_state(
2269 &r#"one
2270 two
2271
2272 three
2273 four
2274 five
2275 ˇ
2276 sixˇ"#
2277 .unindent(),
2278 );
2279
2280 cx.update_editor(|editor, window, cx| {
2281 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2282 });
2283 cx.assert_editor_state(
2284 &r#"one
2285 two
2286
2287 three
2288 four
2289 five
2290
2291 sixˇ"#
2292 .unindent(),
2293 );
2294
2295 cx.update_editor(|editor, window, cx| {
2296 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2297 });
2298 cx.assert_editor_state(
2299 &r#"one
2300 two
2301
2302 three
2303 four
2304 five
2305 ˇ
2306 six"#
2307 .unindent(),
2308 );
2309
2310 cx.update_editor(|editor, window, cx| {
2311 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2312 });
2313 cx.assert_editor_state(
2314 &r#"one
2315 two
2316 ˇ
2317 three
2318 four
2319 five
2320
2321 six"#
2322 .unindent(),
2323 );
2324
2325 cx.update_editor(|editor, window, cx| {
2326 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2327 });
2328 cx.assert_editor_state(
2329 &r#"ˇone
2330 two
2331
2332 three
2333 four
2334 five
2335
2336 six"#
2337 .unindent(),
2338 );
2339}
2340
2341#[gpui::test]
2342async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2343 init_test(cx, |_| {});
2344 let mut cx = EditorTestContext::new(cx).await;
2345 let line_height = cx.editor(|editor, window, _| {
2346 editor
2347 .style()
2348 .unwrap()
2349 .text
2350 .line_height_in_pixels(window.rem_size())
2351 });
2352 let window = cx.window;
2353 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2354
2355 cx.set_state(
2356 r#"ˇone
2357 two
2358 three
2359 four
2360 five
2361 six
2362 seven
2363 eight
2364 nine
2365 ten
2366 "#,
2367 );
2368
2369 cx.update_editor(|editor, window, cx| {
2370 assert_eq!(
2371 editor.snapshot(window, cx).scroll_position(),
2372 gpui::Point::new(0., 0.)
2373 );
2374 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2375 assert_eq!(
2376 editor.snapshot(window, cx).scroll_position(),
2377 gpui::Point::new(0., 3.)
2378 );
2379 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2380 assert_eq!(
2381 editor.snapshot(window, cx).scroll_position(),
2382 gpui::Point::new(0., 6.)
2383 );
2384 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2385 assert_eq!(
2386 editor.snapshot(window, cx).scroll_position(),
2387 gpui::Point::new(0., 3.)
2388 );
2389
2390 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2391 assert_eq!(
2392 editor.snapshot(window, cx).scroll_position(),
2393 gpui::Point::new(0., 1.)
2394 );
2395 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2396 assert_eq!(
2397 editor.snapshot(window, cx).scroll_position(),
2398 gpui::Point::new(0., 3.)
2399 );
2400 });
2401}
2402
2403#[gpui::test]
2404async fn test_autoscroll(cx: &mut TestAppContext) {
2405 init_test(cx, |_| {});
2406 let mut cx = EditorTestContext::new(cx).await;
2407
2408 let line_height = cx.update_editor(|editor, window, cx| {
2409 editor.set_vertical_scroll_margin(2, cx);
2410 editor
2411 .style()
2412 .unwrap()
2413 .text
2414 .line_height_in_pixels(window.rem_size())
2415 });
2416 let window = cx.window;
2417 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2418
2419 cx.set_state(
2420 r#"ˇone
2421 two
2422 three
2423 four
2424 five
2425 six
2426 seven
2427 eight
2428 nine
2429 ten
2430 "#,
2431 );
2432 cx.update_editor(|editor, window, cx| {
2433 assert_eq!(
2434 editor.snapshot(window, cx).scroll_position(),
2435 gpui::Point::new(0., 0.0)
2436 );
2437 });
2438
2439 // Add a cursor below the visible area. Since both cursors cannot fit
2440 // on screen, the editor autoscrolls to reveal the newest cursor, and
2441 // allows the vertical scroll margin below that cursor.
2442 cx.update_editor(|editor, window, cx| {
2443 editor.change_selections(Default::default(), window, cx, |selections| {
2444 selections.select_ranges([
2445 Point::new(0, 0)..Point::new(0, 0),
2446 Point::new(6, 0)..Point::new(6, 0),
2447 ]);
2448 })
2449 });
2450 cx.update_editor(|editor, window, cx| {
2451 assert_eq!(
2452 editor.snapshot(window, cx).scroll_position(),
2453 gpui::Point::new(0., 3.0)
2454 );
2455 });
2456
2457 // Move down. The editor cursor scrolls down to track the newest cursor.
2458 cx.update_editor(|editor, window, cx| {
2459 editor.move_down(&Default::default(), window, cx);
2460 });
2461 cx.update_editor(|editor, window, cx| {
2462 assert_eq!(
2463 editor.snapshot(window, cx).scroll_position(),
2464 gpui::Point::new(0., 4.0)
2465 );
2466 });
2467
2468 // Add a cursor above the visible area. Since both cursors fit on screen,
2469 // the editor scrolls to show both.
2470 cx.update_editor(|editor, window, cx| {
2471 editor.change_selections(Default::default(), window, cx, |selections| {
2472 selections.select_ranges([
2473 Point::new(1, 0)..Point::new(1, 0),
2474 Point::new(6, 0)..Point::new(6, 0),
2475 ]);
2476 })
2477 });
2478 cx.update_editor(|editor, window, cx| {
2479 assert_eq!(
2480 editor.snapshot(window, cx).scroll_position(),
2481 gpui::Point::new(0., 1.0)
2482 );
2483 });
2484}
2485
2486#[gpui::test]
2487async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2488 init_test(cx, |_| {});
2489 let mut cx = EditorTestContext::new(cx).await;
2490
2491 let line_height = cx.editor(|editor, window, _cx| {
2492 editor
2493 .style()
2494 .unwrap()
2495 .text
2496 .line_height_in_pixels(window.rem_size())
2497 });
2498 let window = cx.window;
2499 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2500 cx.set_state(
2501 &r#"
2502 ˇone
2503 two
2504 threeˇ
2505 four
2506 five
2507 six
2508 seven
2509 eight
2510 nine
2511 ten
2512 "#
2513 .unindent(),
2514 );
2515
2516 cx.update_editor(|editor, window, cx| {
2517 editor.move_page_down(&MovePageDown::default(), window, cx)
2518 });
2519 cx.assert_editor_state(
2520 &r#"
2521 one
2522 two
2523 three
2524 ˇfour
2525 five
2526 sixˇ
2527 seven
2528 eight
2529 nine
2530 ten
2531 "#
2532 .unindent(),
2533 );
2534
2535 cx.update_editor(|editor, window, cx| {
2536 editor.move_page_down(&MovePageDown::default(), window, cx)
2537 });
2538 cx.assert_editor_state(
2539 &r#"
2540 one
2541 two
2542 three
2543 four
2544 five
2545 six
2546 ˇseven
2547 eight
2548 nineˇ
2549 ten
2550 "#
2551 .unindent(),
2552 );
2553
2554 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2555 cx.assert_editor_state(
2556 &r#"
2557 one
2558 two
2559 three
2560 ˇfour
2561 five
2562 sixˇ
2563 seven
2564 eight
2565 nine
2566 ten
2567 "#
2568 .unindent(),
2569 );
2570
2571 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2572 cx.assert_editor_state(
2573 &r#"
2574 ˇone
2575 two
2576 threeˇ
2577 four
2578 five
2579 six
2580 seven
2581 eight
2582 nine
2583 ten
2584 "#
2585 .unindent(),
2586 );
2587
2588 // Test select collapsing
2589 cx.update_editor(|editor, window, cx| {
2590 editor.move_page_down(&MovePageDown::default(), window, cx);
2591 editor.move_page_down(&MovePageDown::default(), window, cx);
2592 editor.move_page_down(&MovePageDown::default(), window, cx);
2593 });
2594 cx.assert_editor_state(
2595 &r#"
2596 one
2597 two
2598 three
2599 four
2600 five
2601 six
2602 seven
2603 eight
2604 nine
2605 ˇten
2606 ˇ"#
2607 .unindent(),
2608 );
2609}
2610
2611#[gpui::test]
2612async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2613 init_test(cx, |_| {});
2614 let mut cx = EditorTestContext::new(cx).await;
2615 cx.set_state("one «two threeˇ» four");
2616 cx.update_editor(|editor, window, cx| {
2617 editor.delete_to_beginning_of_line(
2618 &DeleteToBeginningOfLine {
2619 stop_at_indent: false,
2620 },
2621 window,
2622 cx,
2623 );
2624 assert_eq!(editor.text(cx), " four");
2625 });
2626}
2627
2628#[gpui::test]
2629async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2630 init_test(cx, |_| {});
2631
2632 let mut cx = EditorTestContext::new(cx).await;
2633
2634 // For an empty selection, the preceding word fragment is deleted.
2635 // For non-empty selections, only selected characters are deleted.
2636 cx.set_state("onˇe two t«hreˇ»e four");
2637 cx.update_editor(|editor, window, cx| {
2638 editor.delete_to_previous_word_start(
2639 &DeleteToPreviousWordStart {
2640 ignore_newlines: false,
2641 ignore_brackets: false,
2642 },
2643 window,
2644 cx,
2645 );
2646 });
2647 cx.assert_editor_state("ˇe two tˇe four");
2648
2649 cx.set_state("e tˇwo te «fˇ»our");
2650 cx.update_editor(|editor, window, cx| {
2651 editor.delete_to_next_word_end(
2652 &DeleteToNextWordEnd {
2653 ignore_newlines: false,
2654 ignore_brackets: false,
2655 },
2656 window,
2657 cx,
2658 );
2659 });
2660 cx.assert_editor_state("e tˇ te ˇour");
2661}
2662
2663#[gpui::test]
2664async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2665 init_test(cx, |_| {});
2666
2667 let mut cx = EditorTestContext::new(cx).await;
2668
2669 cx.set_state("here is some text ˇwith a space");
2670 cx.update_editor(|editor, window, cx| {
2671 editor.delete_to_previous_word_start(
2672 &DeleteToPreviousWordStart {
2673 ignore_newlines: false,
2674 ignore_brackets: true,
2675 },
2676 window,
2677 cx,
2678 );
2679 });
2680 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2681 cx.assert_editor_state("here is some textˇwith a space");
2682
2683 cx.set_state("here is some text ˇwith a space");
2684 cx.update_editor(|editor, window, cx| {
2685 editor.delete_to_previous_word_start(
2686 &DeleteToPreviousWordStart {
2687 ignore_newlines: false,
2688 ignore_brackets: false,
2689 },
2690 window,
2691 cx,
2692 );
2693 });
2694 cx.assert_editor_state("here is some textˇwith a space");
2695
2696 cx.set_state("here is some textˇ with a space");
2697 cx.update_editor(|editor, window, cx| {
2698 editor.delete_to_next_word_end(
2699 &DeleteToNextWordEnd {
2700 ignore_newlines: false,
2701 ignore_brackets: true,
2702 },
2703 window,
2704 cx,
2705 );
2706 });
2707 // Same happens in the other direction.
2708 cx.assert_editor_state("here is some textˇwith a space");
2709
2710 cx.set_state("here is some textˇ with a space");
2711 cx.update_editor(|editor, window, cx| {
2712 editor.delete_to_next_word_end(
2713 &DeleteToNextWordEnd {
2714 ignore_newlines: false,
2715 ignore_brackets: false,
2716 },
2717 window,
2718 cx,
2719 );
2720 });
2721 cx.assert_editor_state("here is some textˇwith a space");
2722
2723 cx.set_state("here is some textˇ with a space");
2724 cx.update_editor(|editor, window, cx| {
2725 editor.delete_to_next_word_end(
2726 &DeleteToNextWordEnd {
2727 ignore_newlines: true,
2728 ignore_brackets: false,
2729 },
2730 window,
2731 cx,
2732 );
2733 });
2734 cx.assert_editor_state("here is some textˇwith a space");
2735 cx.update_editor(|editor, window, cx| {
2736 editor.delete_to_previous_word_start(
2737 &DeleteToPreviousWordStart {
2738 ignore_newlines: true,
2739 ignore_brackets: false,
2740 },
2741 window,
2742 cx,
2743 );
2744 });
2745 cx.assert_editor_state("here is some ˇwith a space");
2746 cx.update_editor(|editor, window, cx| {
2747 editor.delete_to_previous_word_start(
2748 &DeleteToPreviousWordStart {
2749 ignore_newlines: true,
2750 ignore_brackets: false,
2751 },
2752 window,
2753 cx,
2754 );
2755 });
2756 // Single whitespaces are removed with the word behind them.
2757 cx.assert_editor_state("here is ˇwith a space");
2758 cx.update_editor(|editor, window, cx| {
2759 editor.delete_to_previous_word_start(
2760 &DeleteToPreviousWordStart {
2761 ignore_newlines: true,
2762 ignore_brackets: false,
2763 },
2764 window,
2765 cx,
2766 );
2767 });
2768 cx.assert_editor_state("here ˇwith a space");
2769 cx.update_editor(|editor, window, cx| {
2770 editor.delete_to_previous_word_start(
2771 &DeleteToPreviousWordStart {
2772 ignore_newlines: true,
2773 ignore_brackets: false,
2774 },
2775 window,
2776 cx,
2777 );
2778 });
2779 cx.assert_editor_state("ˇwith a space");
2780 cx.update_editor(|editor, window, cx| {
2781 editor.delete_to_previous_word_start(
2782 &DeleteToPreviousWordStart {
2783 ignore_newlines: true,
2784 ignore_brackets: false,
2785 },
2786 window,
2787 cx,
2788 );
2789 });
2790 cx.assert_editor_state("ˇwith a space");
2791 cx.update_editor(|editor, window, cx| {
2792 editor.delete_to_next_word_end(
2793 &DeleteToNextWordEnd {
2794 ignore_newlines: true,
2795 ignore_brackets: false,
2796 },
2797 window,
2798 cx,
2799 );
2800 });
2801 // Same happens in the other direction.
2802 cx.assert_editor_state("ˇ a space");
2803 cx.update_editor(|editor, window, cx| {
2804 editor.delete_to_next_word_end(
2805 &DeleteToNextWordEnd {
2806 ignore_newlines: true,
2807 ignore_brackets: false,
2808 },
2809 window,
2810 cx,
2811 );
2812 });
2813 cx.assert_editor_state("ˇ space");
2814 cx.update_editor(|editor, window, cx| {
2815 editor.delete_to_next_word_end(
2816 &DeleteToNextWordEnd {
2817 ignore_newlines: true,
2818 ignore_brackets: false,
2819 },
2820 window,
2821 cx,
2822 );
2823 });
2824 cx.assert_editor_state("ˇ");
2825 cx.update_editor(|editor, window, cx| {
2826 editor.delete_to_next_word_end(
2827 &DeleteToNextWordEnd {
2828 ignore_newlines: true,
2829 ignore_brackets: false,
2830 },
2831 window,
2832 cx,
2833 );
2834 });
2835 cx.assert_editor_state("ˇ");
2836 cx.update_editor(|editor, window, cx| {
2837 editor.delete_to_previous_word_start(
2838 &DeleteToPreviousWordStart {
2839 ignore_newlines: true,
2840 ignore_brackets: false,
2841 },
2842 window,
2843 cx,
2844 );
2845 });
2846 cx.assert_editor_state("ˇ");
2847}
2848
2849#[gpui::test]
2850async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2851 init_test(cx, |_| {});
2852
2853 let language = Arc::new(
2854 Language::new(
2855 LanguageConfig {
2856 brackets: BracketPairConfig {
2857 pairs: vec![
2858 BracketPair {
2859 start: "\"".to_string(),
2860 end: "\"".to_string(),
2861 close: true,
2862 surround: true,
2863 newline: false,
2864 },
2865 BracketPair {
2866 start: "(".to_string(),
2867 end: ")".to_string(),
2868 close: true,
2869 surround: true,
2870 newline: true,
2871 },
2872 ],
2873 ..BracketPairConfig::default()
2874 },
2875 ..LanguageConfig::default()
2876 },
2877 Some(tree_sitter_rust::LANGUAGE.into()),
2878 )
2879 .with_brackets_query(
2880 r#"
2881 ("(" @open ")" @close)
2882 ("\"" @open "\"" @close)
2883 "#,
2884 )
2885 .unwrap(),
2886 );
2887
2888 let mut cx = EditorTestContext::new(cx).await;
2889 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2890
2891 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2892 cx.update_editor(|editor, window, cx| {
2893 editor.delete_to_previous_word_start(
2894 &DeleteToPreviousWordStart {
2895 ignore_newlines: true,
2896 ignore_brackets: false,
2897 },
2898 window,
2899 cx,
2900 );
2901 });
2902 // Deletion stops before brackets if asked to not ignore them.
2903 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2904 cx.update_editor(|editor, window, cx| {
2905 editor.delete_to_previous_word_start(
2906 &DeleteToPreviousWordStart {
2907 ignore_newlines: true,
2908 ignore_brackets: false,
2909 },
2910 window,
2911 cx,
2912 );
2913 });
2914 // Deletion has to remove a single bracket and then stop again.
2915 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2916
2917 cx.update_editor(|editor, window, cx| {
2918 editor.delete_to_previous_word_start(
2919 &DeleteToPreviousWordStart {
2920 ignore_newlines: true,
2921 ignore_brackets: false,
2922 },
2923 window,
2924 cx,
2925 );
2926 });
2927 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2928
2929 cx.update_editor(|editor, window, cx| {
2930 editor.delete_to_previous_word_start(
2931 &DeleteToPreviousWordStart {
2932 ignore_newlines: true,
2933 ignore_brackets: false,
2934 },
2935 window,
2936 cx,
2937 );
2938 });
2939 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2940
2941 cx.update_editor(|editor, window, cx| {
2942 editor.delete_to_previous_word_start(
2943 &DeleteToPreviousWordStart {
2944 ignore_newlines: true,
2945 ignore_brackets: false,
2946 },
2947 window,
2948 cx,
2949 );
2950 });
2951 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2952
2953 cx.update_editor(|editor, window, cx| {
2954 editor.delete_to_next_word_end(
2955 &DeleteToNextWordEnd {
2956 ignore_newlines: true,
2957 ignore_brackets: false,
2958 },
2959 window,
2960 cx,
2961 );
2962 });
2963 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2964 cx.assert_editor_state(r#"ˇ");"#);
2965
2966 cx.update_editor(|editor, window, cx| {
2967 editor.delete_to_next_word_end(
2968 &DeleteToNextWordEnd {
2969 ignore_newlines: true,
2970 ignore_brackets: false,
2971 },
2972 window,
2973 cx,
2974 );
2975 });
2976 cx.assert_editor_state(r#"ˇ"#);
2977
2978 cx.update_editor(|editor, window, cx| {
2979 editor.delete_to_next_word_end(
2980 &DeleteToNextWordEnd {
2981 ignore_newlines: true,
2982 ignore_brackets: false,
2983 },
2984 window,
2985 cx,
2986 );
2987 });
2988 cx.assert_editor_state(r#"ˇ"#);
2989
2990 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2991 cx.update_editor(|editor, window, cx| {
2992 editor.delete_to_previous_word_start(
2993 &DeleteToPreviousWordStart {
2994 ignore_newlines: true,
2995 ignore_brackets: true,
2996 },
2997 window,
2998 cx,
2999 );
3000 });
3001 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
3002}
3003
3004#[gpui::test]
3005fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
3006 init_test(cx, |_| {});
3007
3008 let editor = cx.add_window(|window, cx| {
3009 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
3010 build_editor(buffer, window, cx)
3011 });
3012 let del_to_prev_word_start = DeleteToPreviousWordStart {
3013 ignore_newlines: false,
3014 ignore_brackets: false,
3015 };
3016 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
3017 ignore_newlines: true,
3018 ignore_brackets: false,
3019 };
3020
3021 _ = editor.update(cx, |editor, window, cx| {
3022 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3023 s.select_display_ranges([
3024 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
3025 ])
3026 });
3027 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3028 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
3029 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3030 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
3031 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3032 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
3033 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3034 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
3035 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3036 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3037 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3038 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3039 });
3040}
3041
3042#[gpui::test]
3043fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3044 init_test(cx, |_| {});
3045
3046 let editor = cx.add_window(|window, cx| {
3047 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3048 build_editor(buffer, window, cx)
3049 });
3050 let del_to_next_word_end = DeleteToNextWordEnd {
3051 ignore_newlines: false,
3052 ignore_brackets: false,
3053 };
3054 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3055 ignore_newlines: true,
3056 ignore_brackets: false,
3057 };
3058
3059 _ = editor.update(cx, |editor, window, cx| {
3060 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3061 s.select_display_ranges([
3062 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3063 ])
3064 });
3065 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3066 assert_eq!(
3067 editor.buffer.read(cx).read(cx).text(),
3068 "one\n two\nthree\n four"
3069 );
3070 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3071 assert_eq!(
3072 editor.buffer.read(cx).read(cx).text(),
3073 "\n two\nthree\n four"
3074 );
3075 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3076 assert_eq!(
3077 editor.buffer.read(cx).read(cx).text(),
3078 "two\nthree\n four"
3079 );
3080 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3081 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3082 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3083 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3084 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3085 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3086 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3087 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3088 });
3089}
3090
3091#[gpui::test]
3092fn test_newline(cx: &mut TestAppContext) {
3093 init_test(cx, |_| {});
3094
3095 let editor = cx.add_window(|window, cx| {
3096 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3097 build_editor(buffer, window, cx)
3098 });
3099
3100 _ = editor.update(cx, |editor, window, cx| {
3101 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3102 s.select_display_ranges([
3103 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3104 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3105 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3106 ])
3107 });
3108
3109 editor.newline(&Newline, window, cx);
3110 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3111 });
3112}
3113
3114#[gpui::test]
3115fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3116 init_test(cx, |_| {});
3117
3118 let editor = cx.add_window(|window, cx| {
3119 let buffer = MultiBuffer::build_simple(
3120 "
3121 a
3122 b(
3123 X
3124 )
3125 c(
3126 X
3127 )
3128 "
3129 .unindent()
3130 .as_str(),
3131 cx,
3132 );
3133 let mut editor = build_editor(buffer, window, cx);
3134 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3135 s.select_ranges([
3136 Point::new(2, 4)..Point::new(2, 5),
3137 Point::new(5, 4)..Point::new(5, 5),
3138 ])
3139 });
3140 editor
3141 });
3142
3143 _ = editor.update(cx, |editor, window, cx| {
3144 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3145 editor.buffer.update(cx, |buffer, cx| {
3146 buffer.edit(
3147 [
3148 (Point::new(1, 2)..Point::new(3, 0), ""),
3149 (Point::new(4, 2)..Point::new(6, 0), ""),
3150 ],
3151 None,
3152 cx,
3153 );
3154 assert_eq!(
3155 buffer.read(cx).text(),
3156 "
3157 a
3158 b()
3159 c()
3160 "
3161 .unindent()
3162 );
3163 });
3164 assert_eq!(
3165 editor.selections.ranges(cx),
3166 &[
3167 Point::new(1, 2)..Point::new(1, 2),
3168 Point::new(2, 2)..Point::new(2, 2),
3169 ],
3170 );
3171
3172 editor.newline(&Newline, window, cx);
3173 assert_eq!(
3174 editor.text(cx),
3175 "
3176 a
3177 b(
3178 )
3179 c(
3180 )
3181 "
3182 .unindent()
3183 );
3184
3185 // The selections are moved after the inserted newlines
3186 assert_eq!(
3187 editor.selections.ranges(cx),
3188 &[
3189 Point::new(2, 0)..Point::new(2, 0),
3190 Point::new(4, 0)..Point::new(4, 0),
3191 ],
3192 );
3193 });
3194}
3195
3196#[gpui::test]
3197async fn test_newline_above(cx: &mut TestAppContext) {
3198 init_test(cx, |settings| {
3199 settings.defaults.tab_size = NonZeroU32::new(4)
3200 });
3201
3202 let language = Arc::new(
3203 Language::new(
3204 LanguageConfig::default(),
3205 Some(tree_sitter_rust::LANGUAGE.into()),
3206 )
3207 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3208 .unwrap(),
3209 );
3210
3211 let mut cx = EditorTestContext::new(cx).await;
3212 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3213 cx.set_state(indoc! {"
3214 const a: ˇA = (
3215 (ˇ
3216 «const_functionˇ»(ˇ),
3217 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3218 )ˇ
3219 ˇ);ˇ
3220 "});
3221
3222 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3223 cx.assert_editor_state(indoc! {"
3224 ˇ
3225 const a: A = (
3226 ˇ
3227 (
3228 ˇ
3229 ˇ
3230 const_function(),
3231 ˇ
3232 ˇ
3233 ˇ
3234 ˇ
3235 something_else,
3236 ˇ
3237 )
3238 ˇ
3239 ˇ
3240 );
3241 "});
3242}
3243
3244#[gpui::test]
3245async fn test_newline_below(cx: &mut TestAppContext) {
3246 init_test(cx, |settings| {
3247 settings.defaults.tab_size = NonZeroU32::new(4)
3248 });
3249
3250 let language = Arc::new(
3251 Language::new(
3252 LanguageConfig::default(),
3253 Some(tree_sitter_rust::LANGUAGE.into()),
3254 )
3255 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3256 .unwrap(),
3257 );
3258
3259 let mut cx = EditorTestContext::new(cx).await;
3260 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3261 cx.set_state(indoc! {"
3262 const a: ˇA = (
3263 (ˇ
3264 «const_functionˇ»(ˇ),
3265 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3266 )ˇ
3267 ˇ);ˇ
3268 "});
3269
3270 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3271 cx.assert_editor_state(indoc! {"
3272 const a: A = (
3273 ˇ
3274 (
3275 ˇ
3276 const_function(),
3277 ˇ
3278 ˇ
3279 something_else,
3280 ˇ
3281 ˇ
3282 ˇ
3283 ˇ
3284 )
3285 ˇ
3286 );
3287 ˇ
3288 ˇ
3289 "});
3290}
3291
3292#[gpui::test]
3293async fn test_newline_comments(cx: &mut TestAppContext) {
3294 init_test(cx, |settings| {
3295 settings.defaults.tab_size = NonZeroU32::new(4)
3296 });
3297
3298 let language = Arc::new(Language::new(
3299 LanguageConfig {
3300 line_comments: vec!["// ".into()],
3301 ..LanguageConfig::default()
3302 },
3303 None,
3304 ));
3305 {
3306 let mut cx = EditorTestContext::new(cx).await;
3307 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3308 cx.set_state(indoc! {"
3309 // Fooˇ
3310 "});
3311
3312 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3313 cx.assert_editor_state(indoc! {"
3314 // Foo
3315 // ˇ
3316 "});
3317 // Ensure that we add comment prefix when existing line contains space
3318 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3319 cx.assert_editor_state(
3320 indoc! {"
3321 // Foo
3322 //s
3323 // ˇ
3324 "}
3325 .replace("s", " ") // s is used as space placeholder to prevent format on save
3326 .as_str(),
3327 );
3328 // Ensure that we add comment prefix when existing line does not contain space
3329 cx.set_state(indoc! {"
3330 // Foo
3331 //ˇ
3332 "});
3333 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3334 cx.assert_editor_state(indoc! {"
3335 // Foo
3336 //
3337 // ˇ
3338 "});
3339 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3340 cx.set_state(indoc! {"
3341 ˇ// Foo
3342 "});
3343 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3344 cx.assert_editor_state(indoc! {"
3345
3346 ˇ// Foo
3347 "});
3348 }
3349 // Ensure that comment continuations can be disabled.
3350 update_test_language_settings(cx, |settings| {
3351 settings.defaults.extend_comment_on_newline = Some(false);
3352 });
3353 let mut cx = EditorTestContext::new(cx).await;
3354 cx.set_state(indoc! {"
3355 // Fooˇ
3356 "});
3357 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3358 cx.assert_editor_state(indoc! {"
3359 // Foo
3360 ˇ
3361 "});
3362}
3363
3364#[gpui::test]
3365async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3366 init_test(cx, |settings| {
3367 settings.defaults.tab_size = NonZeroU32::new(4)
3368 });
3369
3370 let language = Arc::new(Language::new(
3371 LanguageConfig {
3372 line_comments: vec!["// ".into(), "/// ".into()],
3373 ..LanguageConfig::default()
3374 },
3375 None,
3376 ));
3377 {
3378 let mut cx = EditorTestContext::new(cx).await;
3379 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3380 cx.set_state(indoc! {"
3381 //ˇ
3382 "});
3383 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3384 cx.assert_editor_state(indoc! {"
3385 //
3386 // ˇ
3387 "});
3388
3389 cx.set_state(indoc! {"
3390 ///ˇ
3391 "});
3392 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3393 cx.assert_editor_state(indoc! {"
3394 ///
3395 /// ˇ
3396 "});
3397 }
3398}
3399
3400#[gpui::test]
3401async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3402 init_test(cx, |settings| {
3403 settings.defaults.tab_size = NonZeroU32::new(4)
3404 });
3405
3406 let language = Arc::new(
3407 Language::new(
3408 LanguageConfig {
3409 documentation_comment: Some(language::BlockCommentConfig {
3410 start: "/**".into(),
3411 end: "*/".into(),
3412 prefix: "* ".into(),
3413 tab_size: 1,
3414 }),
3415
3416 ..LanguageConfig::default()
3417 },
3418 Some(tree_sitter_rust::LANGUAGE.into()),
3419 )
3420 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3421 .unwrap(),
3422 );
3423
3424 {
3425 let mut cx = EditorTestContext::new(cx).await;
3426 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3427 cx.set_state(indoc! {"
3428 /**ˇ
3429 "});
3430
3431 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3432 cx.assert_editor_state(indoc! {"
3433 /**
3434 * ˇ
3435 "});
3436 // Ensure that if cursor is before the comment start,
3437 // we do not actually insert a comment prefix.
3438 cx.set_state(indoc! {"
3439 ˇ/**
3440 "});
3441 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3442 cx.assert_editor_state(indoc! {"
3443
3444 ˇ/**
3445 "});
3446 // Ensure that if cursor is between it doesn't add comment prefix.
3447 cx.set_state(indoc! {"
3448 /*ˇ*
3449 "});
3450 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3451 cx.assert_editor_state(indoc! {"
3452 /*
3453 ˇ*
3454 "});
3455 // Ensure that if suffix exists on same line after cursor it adds new line.
3456 cx.set_state(indoc! {"
3457 /**ˇ*/
3458 "});
3459 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3460 cx.assert_editor_state(indoc! {"
3461 /**
3462 * ˇ
3463 */
3464 "});
3465 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3466 cx.set_state(indoc! {"
3467 /**ˇ */
3468 "});
3469 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3470 cx.assert_editor_state(indoc! {"
3471 /**
3472 * ˇ
3473 */
3474 "});
3475 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3476 cx.set_state(indoc! {"
3477 /** ˇ*/
3478 "});
3479 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3480 cx.assert_editor_state(
3481 indoc! {"
3482 /**s
3483 * ˇ
3484 */
3485 "}
3486 .replace("s", " ") // s is used as space placeholder to prevent format on save
3487 .as_str(),
3488 );
3489 // Ensure that delimiter space is preserved when newline on already
3490 // spaced delimiter.
3491 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3492 cx.assert_editor_state(
3493 indoc! {"
3494 /**s
3495 *s
3496 * ˇ
3497 */
3498 "}
3499 .replace("s", " ") // s is used as space placeholder to prevent format on save
3500 .as_str(),
3501 );
3502 // Ensure that delimiter space is preserved when space is not
3503 // on existing delimiter.
3504 cx.set_state(indoc! {"
3505 /**
3506 *ˇ
3507 */
3508 "});
3509 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3510 cx.assert_editor_state(indoc! {"
3511 /**
3512 *
3513 * ˇ
3514 */
3515 "});
3516 // Ensure that if suffix exists on same line after cursor it
3517 // doesn't add extra new line if prefix is not on same line.
3518 cx.set_state(indoc! {"
3519 /**
3520 ˇ*/
3521 "});
3522 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3523 cx.assert_editor_state(indoc! {"
3524 /**
3525
3526 ˇ*/
3527 "});
3528 // Ensure that it detects suffix after existing prefix.
3529 cx.set_state(indoc! {"
3530 /**ˇ/
3531 "});
3532 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3533 cx.assert_editor_state(indoc! {"
3534 /**
3535 ˇ/
3536 "});
3537 // Ensure that if suffix exists on same line before
3538 // cursor it does not add comment prefix.
3539 cx.set_state(indoc! {"
3540 /** */ˇ
3541 "});
3542 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3543 cx.assert_editor_state(indoc! {"
3544 /** */
3545 ˇ
3546 "});
3547 // Ensure that if suffix exists on same line before
3548 // cursor it does not add comment prefix.
3549 cx.set_state(indoc! {"
3550 /**
3551 *
3552 */ˇ
3553 "});
3554 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3555 cx.assert_editor_state(indoc! {"
3556 /**
3557 *
3558 */
3559 ˇ
3560 "});
3561
3562 // Ensure that inline comment followed by code
3563 // doesn't add comment prefix on newline
3564 cx.set_state(indoc! {"
3565 /** */ textˇ
3566 "});
3567 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3568 cx.assert_editor_state(indoc! {"
3569 /** */ text
3570 ˇ
3571 "});
3572
3573 // Ensure that text after comment end tag
3574 // doesn't add comment prefix on newline
3575 cx.set_state(indoc! {"
3576 /**
3577 *
3578 */ˇtext
3579 "});
3580 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3581 cx.assert_editor_state(indoc! {"
3582 /**
3583 *
3584 */
3585 ˇtext
3586 "});
3587
3588 // Ensure if not comment block it doesn't
3589 // add comment prefix on newline
3590 cx.set_state(indoc! {"
3591 * textˇ
3592 "});
3593 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3594 cx.assert_editor_state(indoc! {"
3595 * text
3596 ˇ
3597 "});
3598 }
3599 // Ensure that comment continuations can be disabled.
3600 update_test_language_settings(cx, |settings| {
3601 settings.defaults.extend_comment_on_newline = Some(false);
3602 });
3603 let mut cx = EditorTestContext::new(cx).await;
3604 cx.set_state(indoc! {"
3605 /**ˇ
3606 "});
3607 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3608 cx.assert_editor_state(indoc! {"
3609 /**
3610 ˇ
3611 "});
3612}
3613
3614#[gpui::test]
3615async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3616 init_test(cx, |settings| {
3617 settings.defaults.tab_size = NonZeroU32::new(4)
3618 });
3619
3620 let lua_language = Arc::new(Language::new(
3621 LanguageConfig {
3622 line_comments: vec!["--".into()],
3623 block_comment: Some(language::BlockCommentConfig {
3624 start: "--[[".into(),
3625 prefix: "".into(),
3626 end: "]]".into(),
3627 tab_size: 0,
3628 }),
3629 ..LanguageConfig::default()
3630 },
3631 None,
3632 ));
3633
3634 let mut cx = EditorTestContext::new(cx).await;
3635 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3636
3637 // Line with line comment should extend
3638 cx.set_state(indoc! {"
3639 --ˇ
3640 "});
3641 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3642 cx.assert_editor_state(indoc! {"
3643 --
3644 --ˇ
3645 "});
3646
3647 // Line with block comment that matches line comment should not extend
3648 cx.set_state(indoc! {"
3649 --[[ˇ
3650 "});
3651 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3652 cx.assert_editor_state(indoc! {"
3653 --[[
3654 ˇ
3655 "});
3656}
3657
3658#[gpui::test]
3659fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3660 init_test(cx, |_| {});
3661
3662 let editor = cx.add_window(|window, cx| {
3663 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3664 let mut editor = build_editor(buffer, window, cx);
3665 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3666 s.select_ranges([3..4, 11..12, 19..20])
3667 });
3668 editor
3669 });
3670
3671 _ = editor.update(cx, |editor, window, cx| {
3672 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3673 editor.buffer.update(cx, |buffer, cx| {
3674 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3675 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3676 });
3677 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3678
3679 editor.insert("Z", window, cx);
3680 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3681
3682 // The selections are moved after the inserted characters
3683 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3684 });
3685}
3686
3687#[gpui::test]
3688async fn test_tab(cx: &mut TestAppContext) {
3689 init_test(cx, |settings| {
3690 settings.defaults.tab_size = NonZeroU32::new(3)
3691 });
3692
3693 let mut cx = EditorTestContext::new(cx).await;
3694 cx.set_state(indoc! {"
3695 ˇabˇc
3696 ˇ🏀ˇ🏀ˇefg
3697 dˇ
3698 "});
3699 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3700 cx.assert_editor_state(indoc! {"
3701 ˇab ˇc
3702 ˇ🏀 ˇ🏀 ˇefg
3703 d ˇ
3704 "});
3705
3706 cx.set_state(indoc! {"
3707 a
3708 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3709 "});
3710 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3711 cx.assert_editor_state(indoc! {"
3712 a
3713 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3714 "});
3715}
3716
3717#[gpui::test]
3718async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3719 init_test(cx, |_| {});
3720
3721 let mut cx = EditorTestContext::new(cx).await;
3722 let language = Arc::new(
3723 Language::new(
3724 LanguageConfig::default(),
3725 Some(tree_sitter_rust::LANGUAGE.into()),
3726 )
3727 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3728 .unwrap(),
3729 );
3730 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3731
3732 // test when all cursors are not at suggested indent
3733 // then simply move to their suggested indent location
3734 cx.set_state(indoc! {"
3735 const a: B = (
3736 c(
3737 ˇ
3738 ˇ )
3739 );
3740 "});
3741 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3742 cx.assert_editor_state(indoc! {"
3743 const a: B = (
3744 c(
3745 ˇ
3746 ˇ)
3747 );
3748 "});
3749
3750 // test cursor already at suggested indent not moving when
3751 // other cursors are yet to reach their suggested indents
3752 cx.set_state(indoc! {"
3753 ˇ
3754 const a: B = (
3755 c(
3756 d(
3757 ˇ
3758 )
3759 ˇ
3760 ˇ )
3761 );
3762 "});
3763 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3764 cx.assert_editor_state(indoc! {"
3765 ˇ
3766 const a: B = (
3767 c(
3768 d(
3769 ˇ
3770 )
3771 ˇ
3772 ˇ)
3773 );
3774 "});
3775 // test when all cursors are at suggested indent then tab is inserted
3776 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3777 cx.assert_editor_state(indoc! {"
3778 ˇ
3779 const a: B = (
3780 c(
3781 d(
3782 ˇ
3783 )
3784 ˇ
3785 ˇ)
3786 );
3787 "});
3788
3789 // test when current indent is less than suggested indent,
3790 // we adjust line to match suggested indent and move cursor to it
3791 //
3792 // when no other cursor is at word boundary, all of them should move
3793 cx.set_state(indoc! {"
3794 const a: B = (
3795 c(
3796 d(
3797 ˇ
3798 ˇ )
3799 ˇ )
3800 );
3801 "});
3802 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3803 cx.assert_editor_state(indoc! {"
3804 const a: B = (
3805 c(
3806 d(
3807 ˇ
3808 ˇ)
3809 ˇ)
3810 );
3811 "});
3812
3813 // test when current indent is less than suggested indent,
3814 // we adjust line to match suggested indent and move cursor to it
3815 //
3816 // when some other cursor is at word boundary, it should not move
3817 cx.set_state(indoc! {"
3818 const a: B = (
3819 c(
3820 d(
3821 ˇ
3822 ˇ )
3823 ˇ)
3824 );
3825 "});
3826 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3827 cx.assert_editor_state(indoc! {"
3828 const a: B = (
3829 c(
3830 d(
3831 ˇ
3832 ˇ)
3833 ˇ)
3834 );
3835 "});
3836
3837 // test when current indent is more than suggested indent,
3838 // we just move cursor to current indent instead of suggested indent
3839 //
3840 // when no other cursor is at word boundary, all of them should move
3841 cx.set_state(indoc! {"
3842 const a: B = (
3843 c(
3844 d(
3845 ˇ
3846 ˇ )
3847 ˇ )
3848 );
3849 "});
3850 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3851 cx.assert_editor_state(indoc! {"
3852 const a: B = (
3853 c(
3854 d(
3855 ˇ
3856 ˇ)
3857 ˇ)
3858 );
3859 "});
3860 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3861 cx.assert_editor_state(indoc! {"
3862 const a: B = (
3863 c(
3864 d(
3865 ˇ
3866 ˇ)
3867 ˇ)
3868 );
3869 "});
3870
3871 // test when current indent is more than suggested indent,
3872 // we just move cursor to current indent instead of suggested indent
3873 //
3874 // when some other cursor is at word boundary, it doesn't move
3875 cx.set_state(indoc! {"
3876 const a: B = (
3877 c(
3878 d(
3879 ˇ
3880 ˇ )
3881 ˇ)
3882 );
3883 "});
3884 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3885 cx.assert_editor_state(indoc! {"
3886 const a: B = (
3887 c(
3888 d(
3889 ˇ
3890 ˇ)
3891 ˇ)
3892 );
3893 "});
3894
3895 // handle auto-indent when there are multiple cursors on the same line
3896 cx.set_state(indoc! {"
3897 const a: B = (
3898 c(
3899 ˇ ˇ
3900 ˇ )
3901 );
3902 "});
3903 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3904 cx.assert_editor_state(indoc! {"
3905 const a: B = (
3906 c(
3907 ˇ
3908 ˇ)
3909 );
3910 "});
3911}
3912
3913#[gpui::test]
3914async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3915 init_test(cx, |settings| {
3916 settings.defaults.tab_size = NonZeroU32::new(3)
3917 });
3918
3919 let mut cx = EditorTestContext::new(cx).await;
3920 cx.set_state(indoc! {"
3921 ˇ
3922 \t ˇ
3923 \t ˇ
3924 \t ˇ
3925 \t \t\t \t \t\t \t\t \t \t ˇ
3926 "});
3927
3928 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3929 cx.assert_editor_state(indoc! {"
3930 ˇ
3931 \t ˇ
3932 \t ˇ
3933 \t ˇ
3934 \t \t\t \t \t\t \t\t \t \t ˇ
3935 "});
3936}
3937
3938#[gpui::test]
3939async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3940 init_test(cx, |settings| {
3941 settings.defaults.tab_size = NonZeroU32::new(4)
3942 });
3943
3944 let language = Arc::new(
3945 Language::new(
3946 LanguageConfig::default(),
3947 Some(tree_sitter_rust::LANGUAGE.into()),
3948 )
3949 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3950 .unwrap(),
3951 );
3952
3953 let mut cx = EditorTestContext::new(cx).await;
3954 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3955 cx.set_state(indoc! {"
3956 fn a() {
3957 if b {
3958 \t ˇc
3959 }
3960 }
3961 "});
3962
3963 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3964 cx.assert_editor_state(indoc! {"
3965 fn a() {
3966 if b {
3967 ˇc
3968 }
3969 }
3970 "});
3971}
3972
3973#[gpui::test]
3974async fn test_indent_outdent(cx: &mut TestAppContext) {
3975 init_test(cx, |settings| {
3976 settings.defaults.tab_size = NonZeroU32::new(4);
3977 });
3978
3979 let mut cx = EditorTestContext::new(cx).await;
3980
3981 cx.set_state(indoc! {"
3982 «oneˇ» «twoˇ»
3983 three
3984 four
3985 "});
3986 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3987 cx.assert_editor_state(indoc! {"
3988 «oneˇ» «twoˇ»
3989 three
3990 four
3991 "});
3992
3993 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3994 cx.assert_editor_state(indoc! {"
3995 «oneˇ» «twoˇ»
3996 three
3997 four
3998 "});
3999
4000 // select across line ending
4001 cx.set_state(indoc! {"
4002 one two
4003 t«hree
4004 ˇ» four
4005 "});
4006 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4007 cx.assert_editor_state(indoc! {"
4008 one two
4009 t«hree
4010 ˇ» four
4011 "});
4012
4013 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4014 cx.assert_editor_state(indoc! {"
4015 one two
4016 t«hree
4017 ˇ» four
4018 "});
4019
4020 // Ensure that indenting/outdenting works when the cursor is at column 0.
4021 cx.set_state(indoc! {"
4022 one two
4023 ˇthree
4024 four
4025 "});
4026 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4027 cx.assert_editor_state(indoc! {"
4028 one two
4029 ˇthree
4030 four
4031 "});
4032
4033 cx.set_state(indoc! {"
4034 one two
4035 ˇ three
4036 four
4037 "});
4038 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4039 cx.assert_editor_state(indoc! {"
4040 one two
4041 ˇthree
4042 four
4043 "});
4044}
4045
4046#[gpui::test]
4047async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4048 // This is a regression test for issue #33761
4049 init_test(cx, |_| {});
4050
4051 let mut cx = EditorTestContext::new(cx).await;
4052 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4053 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4054
4055 cx.set_state(
4056 r#"ˇ# ingress:
4057ˇ# api:
4058ˇ# enabled: false
4059ˇ# pathType: Prefix
4060ˇ# console:
4061ˇ# enabled: false
4062ˇ# pathType: Prefix
4063"#,
4064 );
4065
4066 // Press tab to indent all lines
4067 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4068
4069 cx.assert_editor_state(
4070 r#" ˇ# ingress:
4071 ˇ# api:
4072 ˇ# enabled: false
4073 ˇ# pathType: Prefix
4074 ˇ# console:
4075 ˇ# enabled: false
4076 ˇ# pathType: Prefix
4077"#,
4078 );
4079}
4080
4081#[gpui::test]
4082async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4083 // This is a test to make sure our fix for issue #33761 didn't break anything
4084 init_test(cx, |_| {});
4085
4086 let mut cx = EditorTestContext::new(cx).await;
4087 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4088 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4089
4090 cx.set_state(
4091 r#"ˇingress:
4092ˇ api:
4093ˇ enabled: false
4094ˇ pathType: Prefix
4095"#,
4096 );
4097
4098 // Press tab to indent all lines
4099 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4100
4101 cx.assert_editor_state(
4102 r#"ˇingress:
4103 ˇapi:
4104 ˇenabled: false
4105 ˇpathType: Prefix
4106"#,
4107 );
4108}
4109
4110#[gpui::test]
4111async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4112 init_test(cx, |settings| {
4113 settings.defaults.hard_tabs = Some(true);
4114 });
4115
4116 let mut cx = EditorTestContext::new(cx).await;
4117
4118 // select two ranges on one line
4119 cx.set_state(indoc! {"
4120 «oneˇ» «twoˇ»
4121 three
4122 four
4123 "});
4124 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4125 cx.assert_editor_state(indoc! {"
4126 \t«oneˇ» «twoˇ»
4127 three
4128 four
4129 "});
4130 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4131 cx.assert_editor_state(indoc! {"
4132 \t\t«oneˇ» «twoˇ»
4133 three
4134 four
4135 "});
4136 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4137 cx.assert_editor_state(indoc! {"
4138 \t«oneˇ» «twoˇ»
4139 three
4140 four
4141 "});
4142 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4143 cx.assert_editor_state(indoc! {"
4144 «oneˇ» «twoˇ»
4145 three
4146 four
4147 "});
4148
4149 // select across a line ending
4150 cx.set_state(indoc! {"
4151 one two
4152 t«hree
4153 ˇ»four
4154 "});
4155 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4156 cx.assert_editor_state(indoc! {"
4157 one two
4158 \tt«hree
4159 ˇ»four
4160 "});
4161 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4162 cx.assert_editor_state(indoc! {"
4163 one two
4164 \t\tt«hree
4165 ˇ»four
4166 "});
4167 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4168 cx.assert_editor_state(indoc! {"
4169 one two
4170 \tt«hree
4171 ˇ»four
4172 "});
4173 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4174 cx.assert_editor_state(indoc! {"
4175 one two
4176 t«hree
4177 ˇ»four
4178 "});
4179
4180 // Ensure that indenting/outdenting works when the cursor is at column 0.
4181 cx.set_state(indoc! {"
4182 one two
4183 ˇthree
4184 four
4185 "});
4186 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4187 cx.assert_editor_state(indoc! {"
4188 one two
4189 ˇthree
4190 four
4191 "});
4192 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4193 cx.assert_editor_state(indoc! {"
4194 one two
4195 \tˇthree
4196 four
4197 "});
4198 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4199 cx.assert_editor_state(indoc! {"
4200 one two
4201 ˇthree
4202 four
4203 "});
4204}
4205
4206#[gpui::test]
4207fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4208 init_test(cx, |settings| {
4209 settings.languages.0.extend([
4210 (
4211 "TOML".into(),
4212 LanguageSettingsContent {
4213 tab_size: NonZeroU32::new(2),
4214 ..Default::default()
4215 },
4216 ),
4217 (
4218 "Rust".into(),
4219 LanguageSettingsContent {
4220 tab_size: NonZeroU32::new(4),
4221 ..Default::default()
4222 },
4223 ),
4224 ]);
4225 });
4226
4227 let toml_language = Arc::new(Language::new(
4228 LanguageConfig {
4229 name: "TOML".into(),
4230 ..Default::default()
4231 },
4232 None,
4233 ));
4234 let rust_language = Arc::new(Language::new(
4235 LanguageConfig {
4236 name: "Rust".into(),
4237 ..Default::default()
4238 },
4239 None,
4240 ));
4241
4242 let toml_buffer =
4243 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4244 let rust_buffer =
4245 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4246 let multibuffer = cx.new(|cx| {
4247 let mut multibuffer = MultiBuffer::new(ReadWrite);
4248 multibuffer.push_excerpts(
4249 toml_buffer.clone(),
4250 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4251 cx,
4252 );
4253 multibuffer.push_excerpts(
4254 rust_buffer.clone(),
4255 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4256 cx,
4257 );
4258 multibuffer
4259 });
4260
4261 cx.add_window(|window, cx| {
4262 let mut editor = build_editor(multibuffer, window, cx);
4263
4264 assert_eq!(
4265 editor.text(cx),
4266 indoc! {"
4267 a = 1
4268 b = 2
4269
4270 const c: usize = 3;
4271 "}
4272 );
4273
4274 select_ranges(
4275 &mut editor,
4276 indoc! {"
4277 «aˇ» = 1
4278 b = 2
4279
4280 «const c:ˇ» usize = 3;
4281 "},
4282 window,
4283 cx,
4284 );
4285
4286 editor.tab(&Tab, window, cx);
4287 assert_text_with_selections(
4288 &mut editor,
4289 indoc! {"
4290 «aˇ» = 1
4291 b = 2
4292
4293 «const c:ˇ» usize = 3;
4294 "},
4295 cx,
4296 );
4297 editor.backtab(&Backtab, window, cx);
4298 assert_text_with_selections(
4299 &mut editor,
4300 indoc! {"
4301 «aˇ» = 1
4302 b = 2
4303
4304 «const c:ˇ» usize = 3;
4305 "},
4306 cx,
4307 );
4308
4309 editor
4310 });
4311}
4312
4313#[gpui::test]
4314async fn test_backspace(cx: &mut TestAppContext) {
4315 init_test(cx, |_| {});
4316
4317 let mut cx = EditorTestContext::new(cx).await;
4318
4319 // Basic backspace
4320 cx.set_state(indoc! {"
4321 onˇe two three
4322 fou«rˇ» five six
4323 seven «ˇeight nine
4324 »ten
4325 "});
4326 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4327 cx.assert_editor_state(indoc! {"
4328 oˇe two three
4329 fouˇ five six
4330 seven ˇten
4331 "});
4332
4333 // Test backspace inside and around indents
4334 cx.set_state(indoc! {"
4335 zero
4336 ˇone
4337 ˇtwo
4338 ˇ ˇ ˇ three
4339 ˇ ˇ four
4340 "});
4341 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4342 cx.assert_editor_state(indoc! {"
4343 zero
4344 ˇone
4345 ˇtwo
4346 ˇ threeˇ four
4347 "});
4348}
4349
4350#[gpui::test]
4351async fn test_delete(cx: &mut TestAppContext) {
4352 init_test(cx, |_| {});
4353
4354 let mut cx = EditorTestContext::new(cx).await;
4355 cx.set_state(indoc! {"
4356 onˇe two three
4357 fou«rˇ» five six
4358 seven «ˇeight nine
4359 »ten
4360 "});
4361 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4362 cx.assert_editor_state(indoc! {"
4363 onˇ two three
4364 fouˇ five six
4365 seven ˇten
4366 "});
4367}
4368
4369#[gpui::test]
4370fn test_delete_line(cx: &mut TestAppContext) {
4371 init_test(cx, |_| {});
4372
4373 let editor = cx.add_window(|window, cx| {
4374 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4375 build_editor(buffer, window, cx)
4376 });
4377 _ = editor.update(cx, |editor, window, cx| {
4378 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4379 s.select_display_ranges([
4380 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4381 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4382 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4383 ])
4384 });
4385 editor.delete_line(&DeleteLine, window, cx);
4386 assert_eq!(editor.display_text(cx), "ghi");
4387 assert_eq!(
4388 editor.selections.display_ranges(cx),
4389 vec![
4390 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4391 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4392 ]
4393 );
4394 });
4395
4396 let editor = cx.add_window(|window, cx| {
4397 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4398 build_editor(buffer, window, cx)
4399 });
4400 _ = editor.update(cx, |editor, window, cx| {
4401 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4402 s.select_display_ranges([
4403 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4404 ])
4405 });
4406 editor.delete_line(&DeleteLine, window, cx);
4407 assert_eq!(editor.display_text(cx), "ghi\n");
4408 assert_eq!(
4409 editor.selections.display_ranges(cx),
4410 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4411 );
4412 });
4413
4414 let editor = cx.add_window(|window, cx| {
4415 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4416 build_editor(buffer, window, cx)
4417 });
4418 _ = editor.update(cx, |editor, window, cx| {
4419 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4420 s.select_display_ranges([
4421 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4422 ])
4423 });
4424 editor.delete_line(&DeleteLine, window, cx);
4425 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4426 assert_eq!(
4427 editor.selections.display_ranges(cx),
4428 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4429 );
4430 });
4431}
4432
4433#[gpui::test]
4434fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4435 init_test(cx, |_| {});
4436
4437 cx.add_window(|window, cx| {
4438 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4439 let mut editor = build_editor(buffer.clone(), window, cx);
4440 let buffer = buffer.read(cx).as_singleton().unwrap();
4441
4442 assert_eq!(
4443 editor.selections.ranges::<Point>(cx),
4444 &[Point::new(0, 0)..Point::new(0, 0)]
4445 );
4446
4447 // When on single line, replace newline at end by space
4448 editor.join_lines(&JoinLines, window, cx);
4449 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4450 assert_eq!(
4451 editor.selections.ranges::<Point>(cx),
4452 &[Point::new(0, 3)..Point::new(0, 3)]
4453 );
4454
4455 // When multiple lines are selected, remove newlines that are spanned by the selection
4456 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4457 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4458 });
4459 editor.join_lines(&JoinLines, window, cx);
4460 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4461 assert_eq!(
4462 editor.selections.ranges::<Point>(cx),
4463 &[Point::new(0, 11)..Point::new(0, 11)]
4464 );
4465
4466 // Undo should be transactional
4467 editor.undo(&Undo, window, cx);
4468 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4469 assert_eq!(
4470 editor.selections.ranges::<Point>(cx),
4471 &[Point::new(0, 5)..Point::new(2, 2)]
4472 );
4473
4474 // When joining an empty line don't insert a space
4475 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4476 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4477 });
4478 editor.join_lines(&JoinLines, window, cx);
4479 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4480 assert_eq!(
4481 editor.selections.ranges::<Point>(cx),
4482 [Point::new(2, 3)..Point::new(2, 3)]
4483 );
4484
4485 // We can remove trailing newlines
4486 editor.join_lines(&JoinLines, window, cx);
4487 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4488 assert_eq!(
4489 editor.selections.ranges::<Point>(cx),
4490 [Point::new(2, 3)..Point::new(2, 3)]
4491 );
4492
4493 // We don't blow up on the last line
4494 editor.join_lines(&JoinLines, window, cx);
4495 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4496 assert_eq!(
4497 editor.selections.ranges::<Point>(cx),
4498 [Point::new(2, 3)..Point::new(2, 3)]
4499 );
4500
4501 // reset to test indentation
4502 editor.buffer.update(cx, |buffer, cx| {
4503 buffer.edit(
4504 [
4505 (Point::new(1, 0)..Point::new(1, 2), " "),
4506 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4507 ],
4508 None,
4509 cx,
4510 )
4511 });
4512
4513 // We remove any leading spaces
4514 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4515 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4516 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4517 });
4518 editor.join_lines(&JoinLines, window, cx);
4519 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4520
4521 // We don't insert a space for a line containing only spaces
4522 editor.join_lines(&JoinLines, window, cx);
4523 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4524
4525 // We ignore any leading tabs
4526 editor.join_lines(&JoinLines, window, cx);
4527 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4528
4529 editor
4530 });
4531}
4532
4533#[gpui::test]
4534fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4535 init_test(cx, |_| {});
4536
4537 cx.add_window(|window, cx| {
4538 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4539 let mut editor = build_editor(buffer.clone(), window, cx);
4540 let buffer = buffer.read(cx).as_singleton().unwrap();
4541
4542 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4543 s.select_ranges([
4544 Point::new(0, 2)..Point::new(1, 1),
4545 Point::new(1, 2)..Point::new(1, 2),
4546 Point::new(3, 1)..Point::new(3, 2),
4547 ])
4548 });
4549
4550 editor.join_lines(&JoinLines, window, cx);
4551 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4552
4553 assert_eq!(
4554 editor.selections.ranges::<Point>(cx),
4555 [
4556 Point::new(0, 7)..Point::new(0, 7),
4557 Point::new(1, 3)..Point::new(1, 3)
4558 ]
4559 );
4560 editor
4561 });
4562}
4563
4564#[gpui::test]
4565async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4566 init_test(cx, |_| {});
4567
4568 let mut cx = EditorTestContext::new(cx).await;
4569
4570 let diff_base = r#"
4571 Line 0
4572 Line 1
4573 Line 2
4574 Line 3
4575 "#
4576 .unindent();
4577
4578 cx.set_state(
4579 &r#"
4580 ˇLine 0
4581 Line 1
4582 Line 2
4583 Line 3
4584 "#
4585 .unindent(),
4586 );
4587
4588 cx.set_head_text(&diff_base);
4589 executor.run_until_parked();
4590
4591 // Join lines
4592 cx.update_editor(|editor, window, cx| {
4593 editor.join_lines(&JoinLines, window, cx);
4594 });
4595 executor.run_until_parked();
4596
4597 cx.assert_editor_state(
4598 &r#"
4599 Line 0ˇ Line 1
4600 Line 2
4601 Line 3
4602 "#
4603 .unindent(),
4604 );
4605 // Join again
4606 cx.update_editor(|editor, window, cx| {
4607 editor.join_lines(&JoinLines, window, cx);
4608 });
4609 executor.run_until_parked();
4610
4611 cx.assert_editor_state(
4612 &r#"
4613 Line 0 Line 1ˇ Line 2
4614 Line 3
4615 "#
4616 .unindent(),
4617 );
4618}
4619
4620#[gpui::test]
4621async fn test_custom_newlines_cause_no_false_positive_diffs(
4622 executor: BackgroundExecutor,
4623 cx: &mut TestAppContext,
4624) {
4625 init_test(cx, |_| {});
4626 let mut cx = EditorTestContext::new(cx).await;
4627 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4628 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4629 executor.run_until_parked();
4630
4631 cx.update_editor(|editor, window, cx| {
4632 let snapshot = editor.snapshot(window, cx);
4633 assert_eq!(
4634 snapshot
4635 .buffer_snapshot()
4636 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
4637 .collect::<Vec<_>>(),
4638 Vec::new(),
4639 "Should not have any diffs for files with custom newlines"
4640 );
4641 });
4642}
4643
4644#[gpui::test]
4645async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4646 init_test(cx, |_| {});
4647
4648 let mut cx = EditorTestContext::new(cx).await;
4649
4650 // Test sort_lines_case_insensitive()
4651 cx.set_state(indoc! {"
4652 «z
4653 y
4654 x
4655 Z
4656 Y
4657 Xˇ»
4658 "});
4659 cx.update_editor(|e, window, cx| {
4660 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4661 });
4662 cx.assert_editor_state(indoc! {"
4663 «x
4664 X
4665 y
4666 Y
4667 z
4668 Zˇ»
4669 "});
4670
4671 // Test sort_lines_by_length()
4672 //
4673 // Demonstrates:
4674 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4675 // - sort is stable
4676 cx.set_state(indoc! {"
4677 «123
4678 æ
4679 12
4680 ∞
4681 1
4682 æˇ»
4683 "});
4684 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4685 cx.assert_editor_state(indoc! {"
4686 «æ
4687 ∞
4688 1
4689 æ
4690 12
4691 123ˇ»
4692 "});
4693
4694 // Test reverse_lines()
4695 cx.set_state(indoc! {"
4696 «5
4697 4
4698 3
4699 2
4700 1ˇ»
4701 "});
4702 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4703 cx.assert_editor_state(indoc! {"
4704 «1
4705 2
4706 3
4707 4
4708 5ˇ»
4709 "});
4710
4711 // Skip testing shuffle_line()
4712
4713 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4714 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4715
4716 // Don't manipulate when cursor is on single line, but expand the selection
4717 cx.set_state(indoc! {"
4718 ddˇdd
4719 ccc
4720 bb
4721 a
4722 "});
4723 cx.update_editor(|e, window, cx| {
4724 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4725 });
4726 cx.assert_editor_state(indoc! {"
4727 «ddddˇ»
4728 ccc
4729 bb
4730 a
4731 "});
4732
4733 // Basic manipulate case
4734 // Start selection moves to column 0
4735 // End of selection shrinks to fit shorter line
4736 cx.set_state(indoc! {"
4737 dd«d
4738 ccc
4739 bb
4740 aaaaaˇ»
4741 "});
4742 cx.update_editor(|e, window, cx| {
4743 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4744 });
4745 cx.assert_editor_state(indoc! {"
4746 «aaaaa
4747 bb
4748 ccc
4749 dddˇ»
4750 "});
4751
4752 // Manipulate case with newlines
4753 cx.set_state(indoc! {"
4754 dd«d
4755 ccc
4756
4757 bb
4758 aaaaa
4759
4760 ˇ»
4761 "});
4762 cx.update_editor(|e, window, cx| {
4763 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4764 });
4765 cx.assert_editor_state(indoc! {"
4766 «
4767
4768 aaaaa
4769 bb
4770 ccc
4771 dddˇ»
4772
4773 "});
4774
4775 // Adding new line
4776 cx.set_state(indoc! {"
4777 aa«a
4778 bbˇ»b
4779 "});
4780 cx.update_editor(|e, window, cx| {
4781 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4782 });
4783 cx.assert_editor_state(indoc! {"
4784 «aaa
4785 bbb
4786 added_lineˇ»
4787 "});
4788
4789 // Removing line
4790 cx.set_state(indoc! {"
4791 aa«a
4792 bbbˇ»
4793 "});
4794 cx.update_editor(|e, window, cx| {
4795 e.manipulate_immutable_lines(window, cx, |lines| {
4796 lines.pop();
4797 })
4798 });
4799 cx.assert_editor_state(indoc! {"
4800 «aaaˇ»
4801 "});
4802
4803 // Removing all lines
4804 cx.set_state(indoc! {"
4805 aa«a
4806 bbbˇ»
4807 "});
4808 cx.update_editor(|e, window, cx| {
4809 e.manipulate_immutable_lines(window, cx, |lines| {
4810 lines.drain(..);
4811 })
4812 });
4813 cx.assert_editor_state(indoc! {"
4814 ˇ
4815 "});
4816}
4817
4818#[gpui::test]
4819async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4820 init_test(cx, |_| {});
4821
4822 let mut cx = EditorTestContext::new(cx).await;
4823
4824 // Consider continuous selection as single selection
4825 cx.set_state(indoc! {"
4826 Aaa«aa
4827 cˇ»c«c
4828 bb
4829 aaaˇ»aa
4830 "});
4831 cx.update_editor(|e, window, cx| {
4832 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4833 });
4834 cx.assert_editor_state(indoc! {"
4835 «Aaaaa
4836 ccc
4837 bb
4838 aaaaaˇ»
4839 "});
4840
4841 cx.set_state(indoc! {"
4842 Aaa«aa
4843 cˇ»c«c
4844 bb
4845 aaaˇ»aa
4846 "});
4847 cx.update_editor(|e, window, cx| {
4848 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4849 });
4850 cx.assert_editor_state(indoc! {"
4851 «Aaaaa
4852 ccc
4853 bbˇ»
4854 "});
4855
4856 // Consider non continuous selection as distinct dedup operations
4857 cx.set_state(indoc! {"
4858 «aaaaa
4859 bb
4860 aaaaa
4861 aaaaaˇ»
4862
4863 aaa«aaˇ»
4864 "});
4865 cx.update_editor(|e, window, cx| {
4866 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4867 });
4868 cx.assert_editor_state(indoc! {"
4869 «aaaaa
4870 bbˇ»
4871
4872 «aaaaaˇ»
4873 "});
4874}
4875
4876#[gpui::test]
4877async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4878 init_test(cx, |_| {});
4879
4880 let mut cx = EditorTestContext::new(cx).await;
4881
4882 cx.set_state(indoc! {"
4883 «Aaa
4884 aAa
4885 Aaaˇ»
4886 "});
4887 cx.update_editor(|e, window, cx| {
4888 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4889 });
4890 cx.assert_editor_state(indoc! {"
4891 «Aaa
4892 aAaˇ»
4893 "});
4894
4895 cx.set_state(indoc! {"
4896 «Aaa
4897 aAa
4898 aaAˇ»
4899 "});
4900 cx.update_editor(|e, window, cx| {
4901 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4902 });
4903 cx.assert_editor_state(indoc! {"
4904 «Aaaˇ»
4905 "});
4906}
4907
4908#[gpui::test]
4909async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4910 init_test(cx, |_| {});
4911
4912 let mut cx = EditorTestContext::new(cx).await;
4913
4914 let js_language = Arc::new(Language::new(
4915 LanguageConfig {
4916 name: "JavaScript".into(),
4917 wrap_characters: Some(language::WrapCharactersConfig {
4918 start_prefix: "<".into(),
4919 start_suffix: ">".into(),
4920 end_prefix: "</".into(),
4921 end_suffix: ">".into(),
4922 }),
4923 ..LanguageConfig::default()
4924 },
4925 None,
4926 ));
4927
4928 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4929
4930 cx.set_state(indoc! {"
4931 «testˇ»
4932 "});
4933 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4934 cx.assert_editor_state(indoc! {"
4935 <«ˇ»>test</«ˇ»>
4936 "});
4937
4938 cx.set_state(indoc! {"
4939 «test
4940 testˇ»
4941 "});
4942 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4943 cx.assert_editor_state(indoc! {"
4944 <«ˇ»>test
4945 test</«ˇ»>
4946 "});
4947
4948 cx.set_state(indoc! {"
4949 teˇst
4950 "});
4951 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4952 cx.assert_editor_state(indoc! {"
4953 te<«ˇ»></«ˇ»>st
4954 "});
4955}
4956
4957#[gpui::test]
4958async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
4959 init_test(cx, |_| {});
4960
4961 let mut cx = EditorTestContext::new(cx).await;
4962
4963 let js_language = Arc::new(Language::new(
4964 LanguageConfig {
4965 name: "JavaScript".into(),
4966 wrap_characters: Some(language::WrapCharactersConfig {
4967 start_prefix: "<".into(),
4968 start_suffix: ">".into(),
4969 end_prefix: "</".into(),
4970 end_suffix: ">".into(),
4971 }),
4972 ..LanguageConfig::default()
4973 },
4974 None,
4975 ));
4976
4977 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4978
4979 cx.set_state(indoc! {"
4980 «testˇ»
4981 «testˇ» «testˇ»
4982 «testˇ»
4983 "});
4984 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4985 cx.assert_editor_state(indoc! {"
4986 <«ˇ»>test</«ˇ»>
4987 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
4988 <«ˇ»>test</«ˇ»>
4989 "});
4990
4991 cx.set_state(indoc! {"
4992 «test
4993 testˇ»
4994 «test
4995 testˇ»
4996 "});
4997 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4998 cx.assert_editor_state(indoc! {"
4999 <«ˇ»>test
5000 test</«ˇ»>
5001 <«ˇ»>test
5002 test</«ˇ»>
5003 "});
5004}
5005
5006#[gpui::test]
5007async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5008 init_test(cx, |_| {});
5009
5010 let mut cx = EditorTestContext::new(cx).await;
5011
5012 let plaintext_language = Arc::new(Language::new(
5013 LanguageConfig {
5014 name: "Plain Text".into(),
5015 ..LanguageConfig::default()
5016 },
5017 None,
5018 ));
5019
5020 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5021
5022 cx.set_state(indoc! {"
5023 «testˇ»
5024 "});
5025 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5026 cx.assert_editor_state(indoc! {"
5027 «testˇ»
5028 "});
5029}
5030
5031#[gpui::test]
5032async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5033 init_test(cx, |_| {});
5034
5035 let mut cx = EditorTestContext::new(cx).await;
5036
5037 // Manipulate with multiple selections on a single line
5038 cx.set_state(indoc! {"
5039 dd«dd
5040 cˇ»c«c
5041 bb
5042 aaaˇ»aa
5043 "});
5044 cx.update_editor(|e, window, cx| {
5045 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5046 });
5047 cx.assert_editor_state(indoc! {"
5048 «aaaaa
5049 bb
5050 ccc
5051 ddddˇ»
5052 "});
5053
5054 // Manipulate with multiple disjoin selections
5055 cx.set_state(indoc! {"
5056 5«
5057 4
5058 3
5059 2
5060 1ˇ»
5061
5062 dd«dd
5063 ccc
5064 bb
5065 aaaˇ»aa
5066 "});
5067 cx.update_editor(|e, window, cx| {
5068 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5069 });
5070 cx.assert_editor_state(indoc! {"
5071 «1
5072 2
5073 3
5074 4
5075 5ˇ»
5076
5077 «aaaaa
5078 bb
5079 ccc
5080 ddddˇ»
5081 "});
5082
5083 // Adding lines on each selection
5084 cx.set_state(indoc! {"
5085 2«
5086 1ˇ»
5087
5088 bb«bb
5089 aaaˇ»aa
5090 "});
5091 cx.update_editor(|e, window, cx| {
5092 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5093 });
5094 cx.assert_editor_state(indoc! {"
5095 «2
5096 1
5097 added lineˇ»
5098
5099 «bbbb
5100 aaaaa
5101 added lineˇ»
5102 "});
5103
5104 // Removing lines on each selection
5105 cx.set_state(indoc! {"
5106 2«
5107 1ˇ»
5108
5109 bb«bb
5110 aaaˇ»aa
5111 "});
5112 cx.update_editor(|e, window, cx| {
5113 e.manipulate_immutable_lines(window, cx, |lines| {
5114 lines.pop();
5115 })
5116 });
5117 cx.assert_editor_state(indoc! {"
5118 «2ˇ»
5119
5120 «bbbbˇ»
5121 "});
5122}
5123
5124#[gpui::test]
5125async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5126 init_test(cx, |settings| {
5127 settings.defaults.tab_size = NonZeroU32::new(3)
5128 });
5129
5130 let mut cx = EditorTestContext::new(cx).await;
5131
5132 // MULTI SELECTION
5133 // Ln.1 "«" tests empty lines
5134 // Ln.9 tests just leading whitespace
5135 cx.set_state(indoc! {"
5136 «
5137 abc // No indentationˇ»
5138 «\tabc // 1 tabˇ»
5139 \t\tabc « ˇ» // 2 tabs
5140 \t ab«c // Tab followed by space
5141 \tabc // Space followed by tab (3 spaces should be the result)
5142 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5143 abˇ»ˇc ˇ ˇ // Already space indented«
5144 \t
5145 \tabc\tdef // Only the leading tab is manipulatedˇ»
5146 "});
5147 cx.update_editor(|e, window, cx| {
5148 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5149 });
5150 cx.assert_editor_state(
5151 indoc! {"
5152 «
5153 abc // No indentation
5154 abc // 1 tab
5155 abc // 2 tabs
5156 abc // Tab followed by space
5157 abc // Space followed by tab (3 spaces should be the result)
5158 abc // Mixed indentation (tab conversion depends on the column)
5159 abc // Already space indented
5160 ·
5161 abc\tdef // Only the leading tab is manipulatedˇ»
5162 "}
5163 .replace("·", "")
5164 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5165 );
5166
5167 // Test on just a few lines, the others should remain unchanged
5168 // Only lines (3, 5, 10, 11) should change
5169 cx.set_state(
5170 indoc! {"
5171 ·
5172 abc // No indentation
5173 \tabcˇ // 1 tab
5174 \t\tabc // 2 tabs
5175 \t abcˇ // Tab followed by space
5176 \tabc // Space followed by tab (3 spaces should be the result)
5177 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5178 abc // Already space indented
5179 «\t
5180 \tabc\tdef // Only the leading tab is manipulatedˇ»
5181 "}
5182 .replace("·", "")
5183 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5184 );
5185 cx.update_editor(|e, window, cx| {
5186 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5187 });
5188 cx.assert_editor_state(
5189 indoc! {"
5190 ·
5191 abc // No indentation
5192 « abc // 1 tabˇ»
5193 \t\tabc // 2 tabs
5194 « abc // Tab followed by spaceˇ»
5195 \tabc // Space followed by tab (3 spaces should be the result)
5196 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5197 abc // Already space indented
5198 « ·
5199 abc\tdef // Only the leading tab is manipulatedˇ»
5200 "}
5201 .replace("·", "")
5202 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5203 );
5204
5205 // SINGLE SELECTION
5206 // Ln.1 "«" tests empty lines
5207 // Ln.9 tests just leading whitespace
5208 cx.set_state(indoc! {"
5209 «
5210 abc // No indentation
5211 \tabc // 1 tab
5212 \t\tabc // 2 tabs
5213 \t abc // Tab followed by space
5214 \tabc // Space followed by tab (3 spaces should be the result)
5215 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5216 abc // Already space indented
5217 \t
5218 \tabc\tdef // Only the leading tab is manipulatedˇ»
5219 "});
5220 cx.update_editor(|e, window, cx| {
5221 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5222 });
5223 cx.assert_editor_state(
5224 indoc! {"
5225 «
5226 abc // No indentation
5227 abc // 1 tab
5228 abc // 2 tabs
5229 abc // Tab followed by space
5230 abc // Space followed by tab (3 spaces should be the result)
5231 abc // Mixed indentation (tab conversion depends on the column)
5232 abc // Already space indented
5233 ·
5234 abc\tdef // Only the leading tab is manipulatedˇ»
5235 "}
5236 .replace("·", "")
5237 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5238 );
5239}
5240
5241#[gpui::test]
5242async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5243 init_test(cx, |settings| {
5244 settings.defaults.tab_size = NonZeroU32::new(3)
5245 });
5246
5247 let mut cx = EditorTestContext::new(cx).await;
5248
5249 // MULTI SELECTION
5250 // Ln.1 "«" tests empty lines
5251 // Ln.11 tests just leading whitespace
5252 cx.set_state(indoc! {"
5253 «
5254 abˇ»ˇc // No indentation
5255 abc ˇ ˇ // 1 space (< 3 so dont convert)
5256 abc « // 2 spaces (< 3 so dont convert)
5257 abc // 3 spaces (convert)
5258 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5259 «\tˇ»\t«\tˇ»abc // Already tab indented
5260 «\t abc // Tab followed by space
5261 \tabc // Space followed by tab (should be consumed due to tab)
5262 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5263 \tˇ» «\t
5264 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5265 "});
5266 cx.update_editor(|e, window, cx| {
5267 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5268 });
5269 cx.assert_editor_state(indoc! {"
5270 «
5271 abc // No indentation
5272 abc // 1 space (< 3 so dont convert)
5273 abc // 2 spaces (< 3 so dont convert)
5274 \tabc // 3 spaces (convert)
5275 \t abc // 5 spaces (1 tab + 2 spaces)
5276 \t\t\tabc // Already tab indented
5277 \t abc // Tab followed by space
5278 \tabc // Space followed by tab (should be consumed due to tab)
5279 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5280 \t\t\t
5281 \tabc \t // Only the leading spaces should be convertedˇ»
5282 "});
5283
5284 // Test on just a few lines, the other should remain unchanged
5285 // Only lines (4, 8, 11, 12) should change
5286 cx.set_state(
5287 indoc! {"
5288 ·
5289 abc // No indentation
5290 abc // 1 space (< 3 so dont convert)
5291 abc // 2 spaces (< 3 so dont convert)
5292 « abc // 3 spaces (convert)ˇ»
5293 abc // 5 spaces (1 tab + 2 spaces)
5294 \t\t\tabc // Already tab indented
5295 \t abc // Tab followed by space
5296 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5297 \t\t \tabc // Mixed indentation
5298 \t \t \t \tabc // Mixed indentation
5299 \t \tˇ
5300 « abc \t // Only the leading spaces should be convertedˇ»
5301 "}
5302 .replace("·", "")
5303 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5304 );
5305 cx.update_editor(|e, window, cx| {
5306 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5307 });
5308 cx.assert_editor_state(
5309 indoc! {"
5310 ·
5311 abc // No indentation
5312 abc // 1 space (< 3 so dont convert)
5313 abc // 2 spaces (< 3 so dont convert)
5314 «\tabc // 3 spaces (convert)ˇ»
5315 abc // 5 spaces (1 tab + 2 spaces)
5316 \t\t\tabc // Already tab indented
5317 \t abc // Tab followed by space
5318 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5319 \t\t \tabc // Mixed indentation
5320 \t \t \t \tabc // Mixed indentation
5321 «\t\t\t
5322 \tabc \t // Only the leading spaces should be convertedˇ»
5323 "}
5324 .replace("·", "")
5325 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5326 );
5327
5328 // SINGLE SELECTION
5329 // Ln.1 "«" tests empty lines
5330 // Ln.11 tests just leading whitespace
5331 cx.set_state(indoc! {"
5332 «
5333 abc // No indentation
5334 abc // 1 space (< 3 so dont convert)
5335 abc // 2 spaces (< 3 so dont convert)
5336 abc // 3 spaces (convert)
5337 abc // 5 spaces (1 tab + 2 spaces)
5338 \t\t\tabc // Already tab indented
5339 \t abc // Tab followed by space
5340 \tabc // Space followed by tab (should be consumed due to tab)
5341 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5342 \t \t
5343 abc \t // Only the leading spaces should be convertedˇ»
5344 "});
5345 cx.update_editor(|e, window, cx| {
5346 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5347 });
5348 cx.assert_editor_state(indoc! {"
5349 «
5350 abc // No indentation
5351 abc // 1 space (< 3 so dont convert)
5352 abc // 2 spaces (< 3 so dont convert)
5353 \tabc // 3 spaces (convert)
5354 \t abc // 5 spaces (1 tab + 2 spaces)
5355 \t\t\tabc // Already tab indented
5356 \t abc // Tab followed by space
5357 \tabc // Space followed by tab (should be consumed due to tab)
5358 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5359 \t\t\t
5360 \tabc \t // Only the leading spaces should be convertedˇ»
5361 "});
5362}
5363
5364#[gpui::test]
5365async fn test_toggle_case(cx: &mut TestAppContext) {
5366 init_test(cx, |_| {});
5367
5368 let mut cx = EditorTestContext::new(cx).await;
5369
5370 // If all lower case -> upper case
5371 cx.set_state(indoc! {"
5372 «hello worldˇ»
5373 "});
5374 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5375 cx.assert_editor_state(indoc! {"
5376 «HELLO WORLDˇ»
5377 "});
5378
5379 // If all upper case -> lower case
5380 cx.set_state(indoc! {"
5381 «HELLO WORLDˇ»
5382 "});
5383 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5384 cx.assert_editor_state(indoc! {"
5385 «hello worldˇ»
5386 "});
5387
5388 // If any upper case characters are identified -> lower case
5389 // This matches JetBrains IDEs
5390 cx.set_state(indoc! {"
5391 «hEllo worldˇ»
5392 "});
5393 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5394 cx.assert_editor_state(indoc! {"
5395 «hello worldˇ»
5396 "});
5397}
5398
5399#[gpui::test]
5400async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5401 init_test(cx, |_| {});
5402
5403 let mut cx = EditorTestContext::new(cx).await;
5404
5405 cx.set_state(indoc! {"
5406 «implement-windows-supportˇ»
5407 "});
5408 cx.update_editor(|e, window, cx| {
5409 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5410 });
5411 cx.assert_editor_state(indoc! {"
5412 «Implement windows supportˇ»
5413 "});
5414}
5415
5416#[gpui::test]
5417async fn test_manipulate_text(cx: &mut TestAppContext) {
5418 init_test(cx, |_| {});
5419
5420 let mut cx = EditorTestContext::new(cx).await;
5421
5422 // Test convert_to_upper_case()
5423 cx.set_state(indoc! {"
5424 «hello worldˇ»
5425 "});
5426 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5427 cx.assert_editor_state(indoc! {"
5428 «HELLO WORLDˇ»
5429 "});
5430
5431 // Test convert_to_lower_case()
5432 cx.set_state(indoc! {"
5433 «HELLO WORLDˇ»
5434 "});
5435 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5436 cx.assert_editor_state(indoc! {"
5437 «hello worldˇ»
5438 "});
5439
5440 // Test multiple line, single selection case
5441 cx.set_state(indoc! {"
5442 «The quick brown
5443 fox jumps over
5444 the lazy dogˇ»
5445 "});
5446 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5447 cx.assert_editor_state(indoc! {"
5448 «The Quick Brown
5449 Fox Jumps Over
5450 The Lazy Dogˇ»
5451 "});
5452
5453 // Test multiple line, single selection case
5454 cx.set_state(indoc! {"
5455 «The quick brown
5456 fox jumps over
5457 the lazy dogˇ»
5458 "});
5459 cx.update_editor(|e, window, cx| {
5460 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5461 });
5462 cx.assert_editor_state(indoc! {"
5463 «TheQuickBrown
5464 FoxJumpsOver
5465 TheLazyDogˇ»
5466 "});
5467
5468 // From here on out, test more complex cases of manipulate_text()
5469
5470 // Test no selection case - should affect words cursors are in
5471 // Cursor at beginning, middle, and end of word
5472 cx.set_state(indoc! {"
5473 ˇhello big beauˇtiful worldˇ
5474 "});
5475 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5476 cx.assert_editor_state(indoc! {"
5477 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5478 "});
5479
5480 // Test multiple selections on a single line and across multiple lines
5481 cx.set_state(indoc! {"
5482 «Theˇ» quick «brown
5483 foxˇ» jumps «overˇ»
5484 the «lazyˇ» dog
5485 "});
5486 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5487 cx.assert_editor_state(indoc! {"
5488 «THEˇ» quick «BROWN
5489 FOXˇ» jumps «OVERˇ»
5490 the «LAZYˇ» dog
5491 "});
5492
5493 // Test case where text length grows
5494 cx.set_state(indoc! {"
5495 «tschüߡ»
5496 "});
5497 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5498 cx.assert_editor_state(indoc! {"
5499 «TSCHÜSSˇ»
5500 "});
5501
5502 // Test to make sure we don't crash when text shrinks
5503 cx.set_state(indoc! {"
5504 aaa_bbbˇ
5505 "});
5506 cx.update_editor(|e, window, cx| {
5507 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5508 });
5509 cx.assert_editor_state(indoc! {"
5510 «aaaBbbˇ»
5511 "});
5512
5513 // Test to make sure we all aware of the fact that each word can grow and shrink
5514 // Final selections should be aware of this fact
5515 cx.set_state(indoc! {"
5516 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5517 "});
5518 cx.update_editor(|e, window, cx| {
5519 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5520 });
5521 cx.assert_editor_state(indoc! {"
5522 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5523 "});
5524
5525 cx.set_state(indoc! {"
5526 «hElLo, WoRld!ˇ»
5527 "});
5528 cx.update_editor(|e, window, cx| {
5529 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5530 });
5531 cx.assert_editor_state(indoc! {"
5532 «HeLlO, wOrLD!ˇ»
5533 "});
5534
5535 // Test selections with `line_mode() = true`.
5536 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5537 cx.set_state(indoc! {"
5538 «The quick brown
5539 fox jumps over
5540 tˇ»he lazy dog
5541 "});
5542 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5543 cx.assert_editor_state(indoc! {"
5544 «THE QUICK BROWN
5545 FOX JUMPS OVER
5546 THE LAZY DOGˇ»
5547 "});
5548}
5549
5550#[gpui::test]
5551fn test_duplicate_line(cx: &mut TestAppContext) {
5552 init_test(cx, |_| {});
5553
5554 let editor = cx.add_window(|window, cx| {
5555 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5556 build_editor(buffer, window, cx)
5557 });
5558 _ = editor.update(cx, |editor, window, cx| {
5559 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5560 s.select_display_ranges([
5561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5562 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5563 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5564 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5565 ])
5566 });
5567 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5568 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5569 assert_eq!(
5570 editor.selections.display_ranges(cx),
5571 vec![
5572 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5573 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5574 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5575 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5576 ]
5577 );
5578 });
5579
5580 let editor = cx.add_window(|window, cx| {
5581 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5582 build_editor(buffer, window, cx)
5583 });
5584 _ = editor.update(cx, |editor, window, cx| {
5585 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5586 s.select_display_ranges([
5587 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5588 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5589 ])
5590 });
5591 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5592 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5593 assert_eq!(
5594 editor.selections.display_ranges(cx),
5595 vec![
5596 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5597 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5598 ]
5599 );
5600 });
5601
5602 // With `move_upwards` the selections stay in place, except for
5603 // the lines inserted above them
5604 let editor = cx.add_window(|window, cx| {
5605 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5606 build_editor(buffer, window, cx)
5607 });
5608 _ = editor.update(cx, |editor, window, cx| {
5609 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5610 s.select_display_ranges([
5611 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5612 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5613 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5614 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5615 ])
5616 });
5617 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5618 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5619 assert_eq!(
5620 editor.selections.display_ranges(cx),
5621 vec![
5622 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5623 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5624 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5625 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5626 ]
5627 );
5628 });
5629
5630 let editor = cx.add_window(|window, cx| {
5631 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5632 build_editor(buffer, window, cx)
5633 });
5634 _ = editor.update(cx, |editor, window, cx| {
5635 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5636 s.select_display_ranges([
5637 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5638 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5639 ])
5640 });
5641 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5642 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5643 assert_eq!(
5644 editor.selections.display_ranges(cx),
5645 vec![
5646 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5647 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5648 ]
5649 );
5650 });
5651
5652 let editor = cx.add_window(|window, cx| {
5653 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5654 build_editor(buffer, window, cx)
5655 });
5656 _ = editor.update(cx, |editor, window, cx| {
5657 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5658 s.select_display_ranges([
5659 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5660 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5661 ])
5662 });
5663 editor.duplicate_selection(&DuplicateSelection, window, cx);
5664 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5665 assert_eq!(
5666 editor.selections.display_ranges(cx),
5667 vec![
5668 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5669 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5670 ]
5671 );
5672 });
5673}
5674
5675#[gpui::test]
5676fn test_move_line_up_down(cx: &mut TestAppContext) {
5677 init_test(cx, |_| {});
5678
5679 let editor = cx.add_window(|window, cx| {
5680 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5681 build_editor(buffer, window, cx)
5682 });
5683 _ = editor.update(cx, |editor, window, cx| {
5684 editor.fold_creases(
5685 vec![
5686 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5687 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5688 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5689 ],
5690 true,
5691 window,
5692 cx,
5693 );
5694 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5695 s.select_display_ranges([
5696 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5697 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5698 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5699 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5700 ])
5701 });
5702 assert_eq!(
5703 editor.display_text(cx),
5704 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5705 );
5706
5707 editor.move_line_up(&MoveLineUp, window, cx);
5708 assert_eq!(
5709 editor.display_text(cx),
5710 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5711 );
5712 assert_eq!(
5713 editor.selections.display_ranges(cx),
5714 vec![
5715 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5716 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5717 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5718 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5719 ]
5720 );
5721 });
5722
5723 _ = editor.update(cx, |editor, window, cx| {
5724 editor.move_line_down(&MoveLineDown, window, cx);
5725 assert_eq!(
5726 editor.display_text(cx),
5727 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5728 );
5729 assert_eq!(
5730 editor.selections.display_ranges(cx),
5731 vec![
5732 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5733 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5734 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5735 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5736 ]
5737 );
5738 });
5739
5740 _ = editor.update(cx, |editor, window, cx| {
5741 editor.move_line_down(&MoveLineDown, window, cx);
5742 assert_eq!(
5743 editor.display_text(cx),
5744 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5745 );
5746 assert_eq!(
5747 editor.selections.display_ranges(cx),
5748 vec![
5749 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5750 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5751 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5752 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5753 ]
5754 );
5755 });
5756
5757 _ = editor.update(cx, |editor, window, cx| {
5758 editor.move_line_up(&MoveLineUp, window, cx);
5759 assert_eq!(
5760 editor.display_text(cx),
5761 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5762 );
5763 assert_eq!(
5764 editor.selections.display_ranges(cx),
5765 vec![
5766 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5767 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5768 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5769 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5770 ]
5771 );
5772 });
5773}
5774
5775#[gpui::test]
5776fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5777 init_test(cx, |_| {});
5778 let editor = cx.add_window(|window, cx| {
5779 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5780 build_editor(buffer, window, cx)
5781 });
5782 _ = editor.update(cx, |editor, window, cx| {
5783 editor.fold_creases(
5784 vec![Crease::simple(
5785 Point::new(6, 4)..Point::new(7, 4),
5786 FoldPlaceholder::test(),
5787 )],
5788 true,
5789 window,
5790 cx,
5791 );
5792 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5793 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5794 });
5795 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5796 editor.move_line_up(&MoveLineUp, window, cx);
5797 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5798 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5799 });
5800}
5801
5802#[gpui::test]
5803fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5804 init_test(cx, |_| {});
5805
5806 let editor = cx.add_window(|window, cx| {
5807 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5808 build_editor(buffer, window, cx)
5809 });
5810 _ = editor.update(cx, |editor, window, cx| {
5811 let snapshot = editor.buffer.read(cx).snapshot(cx);
5812 editor.insert_blocks(
5813 [BlockProperties {
5814 style: BlockStyle::Fixed,
5815 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5816 height: Some(1),
5817 render: Arc::new(|_| div().into_any()),
5818 priority: 0,
5819 }],
5820 Some(Autoscroll::fit()),
5821 cx,
5822 );
5823 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5824 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5825 });
5826 editor.move_line_down(&MoveLineDown, window, cx);
5827 });
5828}
5829
5830#[gpui::test]
5831async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5832 init_test(cx, |_| {});
5833
5834 let mut cx = EditorTestContext::new(cx).await;
5835 cx.set_state(
5836 &"
5837 ˇzero
5838 one
5839 two
5840 three
5841 four
5842 five
5843 "
5844 .unindent(),
5845 );
5846
5847 // Create a four-line block that replaces three lines of text.
5848 cx.update_editor(|editor, window, cx| {
5849 let snapshot = editor.snapshot(window, cx);
5850 let snapshot = &snapshot.buffer_snapshot();
5851 let placement = BlockPlacement::Replace(
5852 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5853 );
5854 editor.insert_blocks(
5855 [BlockProperties {
5856 placement,
5857 height: Some(4),
5858 style: BlockStyle::Sticky,
5859 render: Arc::new(|_| gpui::div().into_any_element()),
5860 priority: 0,
5861 }],
5862 None,
5863 cx,
5864 );
5865 });
5866
5867 // Move down so that the cursor touches the block.
5868 cx.update_editor(|editor, window, cx| {
5869 editor.move_down(&Default::default(), window, cx);
5870 });
5871 cx.assert_editor_state(
5872 &"
5873 zero
5874 «one
5875 two
5876 threeˇ»
5877 four
5878 five
5879 "
5880 .unindent(),
5881 );
5882
5883 // Move down past the block.
5884 cx.update_editor(|editor, window, cx| {
5885 editor.move_down(&Default::default(), window, cx);
5886 });
5887 cx.assert_editor_state(
5888 &"
5889 zero
5890 one
5891 two
5892 three
5893 ˇfour
5894 five
5895 "
5896 .unindent(),
5897 );
5898}
5899
5900#[gpui::test]
5901fn test_transpose(cx: &mut TestAppContext) {
5902 init_test(cx, |_| {});
5903
5904 _ = cx.add_window(|window, cx| {
5905 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5906 editor.set_style(EditorStyle::default(), window, cx);
5907 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5908 s.select_ranges([1..1])
5909 });
5910 editor.transpose(&Default::default(), window, cx);
5911 assert_eq!(editor.text(cx), "bac");
5912 assert_eq!(editor.selections.ranges(cx), [2..2]);
5913
5914 editor.transpose(&Default::default(), window, cx);
5915 assert_eq!(editor.text(cx), "bca");
5916 assert_eq!(editor.selections.ranges(cx), [3..3]);
5917
5918 editor.transpose(&Default::default(), window, cx);
5919 assert_eq!(editor.text(cx), "bac");
5920 assert_eq!(editor.selections.ranges(cx), [3..3]);
5921
5922 editor
5923 });
5924
5925 _ = cx.add_window(|window, cx| {
5926 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5927 editor.set_style(EditorStyle::default(), window, cx);
5928 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5929 s.select_ranges([3..3])
5930 });
5931 editor.transpose(&Default::default(), window, cx);
5932 assert_eq!(editor.text(cx), "acb\nde");
5933 assert_eq!(editor.selections.ranges(cx), [3..3]);
5934
5935 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5936 s.select_ranges([4..4])
5937 });
5938 editor.transpose(&Default::default(), window, cx);
5939 assert_eq!(editor.text(cx), "acbd\ne");
5940 assert_eq!(editor.selections.ranges(cx), [5..5]);
5941
5942 editor.transpose(&Default::default(), window, cx);
5943 assert_eq!(editor.text(cx), "acbde\n");
5944 assert_eq!(editor.selections.ranges(cx), [6..6]);
5945
5946 editor.transpose(&Default::default(), window, cx);
5947 assert_eq!(editor.text(cx), "acbd\ne");
5948 assert_eq!(editor.selections.ranges(cx), [6..6]);
5949
5950 editor
5951 });
5952
5953 _ = cx.add_window(|window, cx| {
5954 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5955 editor.set_style(EditorStyle::default(), window, cx);
5956 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5957 s.select_ranges([1..1, 2..2, 4..4])
5958 });
5959 editor.transpose(&Default::default(), window, cx);
5960 assert_eq!(editor.text(cx), "bacd\ne");
5961 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5962
5963 editor.transpose(&Default::default(), window, cx);
5964 assert_eq!(editor.text(cx), "bcade\n");
5965 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5966
5967 editor.transpose(&Default::default(), window, cx);
5968 assert_eq!(editor.text(cx), "bcda\ne");
5969 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5970
5971 editor.transpose(&Default::default(), window, cx);
5972 assert_eq!(editor.text(cx), "bcade\n");
5973 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5974
5975 editor.transpose(&Default::default(), window, cx);
5976 assert_eq!(editor.text(cx), "bcaed\n");
5977 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5978
5979 editor
5980 });
5981
5982 _ = cx.add_window(|window, cx| {
5983 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5984 editor.set_style(EditorStyle::default(), window, cx);
5985 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5986 s.select_ranges([4..4])
5987 });
5988 editor.transpose(&Default::default(), window, cx);
5989 assert_eq!(editor.text(cx), "🏀🍐✋");
5990 assert_eq!(editor.selections.ranges(cx), [8..8]);
5991
5992 editor.transpose(&Default::default(), window, cx);
5993 assert_eq!(editor.text(cx), "🏀✋🍐");
5994 assert_eq!(editor.selections.ranges(cx), [11..11]);
5995
5996 editor.transpose(&Default::default(), window, cx);
5997 assert_eq!(editor.text(cx), "🏀🍐✋");
5998 assert_eq!(editor.selections.ranges(cx), [11..11]);
5999
6000 editor
6001 });
6002}
6003
6004#[gpui::test]
6005async fn test_rewrap(cx: &mut TestAppContext) {
6006 init_test(cx, |settings| {
6007 settings.languages.0.extend([
6008 (
6009 "Markdown".into(),
6010 LanguageSettingsContent {
6011 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6012 preferred_line_length: Some(40),
6013 ..Default::default()
6014 },
6015 ),
6016 (
6017 "Plain Text".into(),
6018 LanguageSettingsContent {
6019 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6020 preferred_line_length: Some(40),
6021 ..Default::default()
6022 },
6023 ),
6024 (
6025 "C++".into(),
6026 LanguageSettingsContent {
6027 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6028 preferred_line_length: Some(40),
6029 ..Default::default()
6030 },
6031 ),
6032 (
6033 "Python".into(),
6034 LanguageSettingsContent {
6035 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6036 preferred_line_length: Some(40),
6037 ..Default::default()
6038 },
6039 ),
6040 (
6041 "Rust".into(),
6042 LanguageSettingsContent {
6043 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6044 preferred_line_length: Some(40),
6045 ..Default::default()
6046 },
6047 ),
6048 ])
6049 });
6050
6051 let mut cx = EditorTestContext::new(cx).await;
6052
6053 let cpp_language = Arc::new(Language::new(
6054 LanguageConfig {
6055 name: "C++".into(),
6056 line_comments: vec!["// ".into()],
6057 ..LanguageConfig::default()
6058 },
6059 None,
6060 ));
6061 let python_language = Arc::new(Language::new(
6062 LanguageConfig {
6063 name: "Python".into(),
6064 line_comments: vec!["# ".into()],
6065 ..LanguageConfig::default()
6066 },
6067 None,
6068 ));
6069 let markdown_language = Arc::new(Language::new(
6070 LanguageConfig {
6071 name: "Markdown".into(),
6072 rewrap_prefixes: vec![
6073 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6074 regex::Regex::new("[-*+]\\s+").unwrap(),
6075 ],
6076 ..LanguageConfig::default()
6077 },
6078 None,
6079 ));
6080 let rust_language = Arc::new(
6081 Language::new(
6082 LanguageConfig {
6083 name: "Rust".into(),
6084 line_comments: vec!["// ".into(), "/// ".into()],
6085 ..LanguageConfig::default()
6086 },
6087 Some(tree_sitter_rust::LANGUAGE.into()),
6088 )
6089 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6090 .unwrap(),
6091 );
6092
6093 let plaintext_language = Arc::new(Language::new(
6094 LanguageConfig {
6095 name: "Plain Text".into(),
6096 ..LanguageConfig::default()
6097 },
6098 None,
6099 ));
6100
6101 // Test basic rewrapping of a long line with a cursor
6102 assert_rewrap(
6103 indoc! {"
6104 // ˇThis is a long comment that needs to be wrapped.
6105 "},
6106 indoc! {"
6107 // ˇThis is a long comment that needs to
6108 // be wrapped.
6109 "},
6110 cpp_language.clone(),
6111 &mut cx,
6112 );
6113
6114 // Test rewrapping a full selection
6115 assert_rewrap(
6116 indoc! {"
6117 «// This selected long comment needs to be wrapped.ˇ»"
6118 },
6119 indoc! {"
6120 «// This selected long comment needs to
6121 // be wrapped.ˇ»"
6122 },
6123 cpp_language.clone(),
6124 &mut cx,
6125 );
6126
6127 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6128 assert_rewrap(
6129 indoc! {"
6130 // ˇThis is the first line.
6131 // Thisˇ is the second line.
6132 // This is the thirdˇ line, all part of one paragraph.
6133 "},
6134 indoc! {"
6135 // ˇThis is the first line. Thisˇ is the
6136 // second line. This is the thirdˇ line,
6137 // all part of one paragraph.
6138 "},
6139 cpp_language.clone(),
6140 &mut cx,
6141 );
6142
6143 // Test multiple cursors in different paragraphs trigger separate rewraps
6144 assert_rewrap(
6145 indoc! {"
6146 // ˇThis is the first paragraph, first line.
6147 // ˇThis is the first paragraph, second line.
6148
6149 // ˇThis is the second paragraph, first line.
6150 // ˇThis is the second paragraph, second line.
6151 "},
6152 indoc! {"
6153 // ˇThis is the first paragraph, first
6154 // line. ˇThis is the first paragraph,
6155 // second line.
6156
6157 // ˇThis is the second paragraph, first
6158 // line. ˇThis is the second paragraph,
6159 // second line.
6160 "},
6161 cpp_language.clone(),
6162 &mut cx,
6163 );
6164
6165 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6166 assert_rewrap(
6167 indoc! {"
6168 «// A regular long long comment to be wrapped.
6169 /// A documentation long comment to be wrapped.ˇ»
6170 "},
6171 indoc! {"
6172 «// A regular long long comment to be
6173 // wrapped.
6174 /// A documentation long comment to be
6175 /// wrapped.ˇ»
6176 "},
6177 rust_language.clone(),
6178 &mut cx,
6179 );
6180
6181 // Test that change in indentation level trigger seperate rewraps
6182 assert_rewrap(
6183 indoc! {"
6184 fn foo() {
6185 «// This is a long comment at the base indent.
6186 // This is a long comment at the next indent.ˇ»
6187 }
6188 "},
6189 indoc! {"
6190 fn foo() {
6191 «// This is a long comment at the
6192 // base indent.
6193 // This is a long comment at the
6194 // next indent.ˇ»
6195 }
6196 "},
6197 rust_language.clone(),
6198 &mut cx,
6199 );
6200
6201 // Test that different comment prefix characters (e.g., '#') are handled correctly
6202 assert_rewrap(
6203 indoc! {"
6204 # ˇThis is a long comment using a pound sign.
6205 "},
6206 indoc! {"
6207 # ˇThis is a long comment using a pound
6208 # sign.
6209 "},
6210 python_language,
6211 &mut cx,
6212 );
6213
6214 // Test rewrapping only affects comments, not code even when selected
6215 assert_rewrap(
6216 indoc! {"
6217 «/// This doc comment is long and should be wrapped.
6218 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6219 "},
6220 indoc! {"
6221 «/// This doc comment is long and should
6222 /// be wrapped.
6223 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6224 "},
6225 rust_language.clone(),
6226 &mut cx,
6227 );
6228
6229 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6230 assert_rewrap(
6231 indoc! {"
6232 # Header
6233
6234 A long long long line of markdown text to wrap.ˇ
6235 "},
6236 indoc! {"
6237 # Header
6238
6239 A long long long line of markdown text
6240 to wrap.ˇ
6241 "},
6242 markdown_language.clone(),
6243 &mut cx,
6244 );
6245
6246 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6247 assert_rewrap(
6248 indoc! {"
6249 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6250 2. This is a numbered list item that is very long and needs to be wrapped properly.
6251 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6252 "},
6253 indoc! {"
6254 «1. This is a numbered list item that is
6255 very long and needs to be wrapped
6256 properly.
6257 2. This is a numbered list item that is
6258 very long and needs to be wrapped
6259 properly.
6260 - This is an unordered list item that is
6261 also very long and should not merge
6262 with the numbered item.ˇ»
6263 "},
6264 markdown_language.clone(),
6265 &mut cx,
6266 );
6267
6268 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6269 assert_rewrap(
6270 indoc! {"
6271 «1. This is a numbered list item that is
6272 very long and needs to be wrapped
6273 properly.
6274 2. This is a numbered list item that is
6275 very long and needs to be wrapped
6276 properly.
6277 - This is an unordered list item that is
6278 also very long and should not merge with
6279 the numbered item.ˇ»
6280 "},
6281 indoc! {"
6282 «1. This is a numbered list item that is
6283 very long and needs to be wrapped
6284 properly.
6285 2. This is a numbered list item that is
6286 very long and needs to be wrapped
6287 properly.
6288 - This is an unordered list item that is
6289 also very long and should not merge
6290 with the numbered item.ˇ»
6291 "},
6292 markdown_language.clone(),
6293 &mut cx,
6294 );
6295
6296 // Test that rewrapping maintain indents even when they already exists.
6297 assert_rewrap(
6298 indoc! {"
6299 «1. This is a numbered list
6300 item that is very long and needs to be wrapped properly.
6301 2. This is a numbered list
6302 item that is very long and needs to be wrapped properly.
6303 - This is an unordered list item that is also very long and
6304 should not merge with the numbered item.ˇ»
6305 "},
6306 indoc! {"
6307 «1. This is a numbered list item that is
6308 very long and needs to be wrapped
6309 properly.
6310 2. This is a numbered list item that is
6311 very long and needs to be wrapped
6312 properly.
6313 - This is an unordered list item that is
6314 also very long and should not merge
6315 with the numbered item.ˇ»
6316 "},
6317 markdown_language,
6318 &mut cx,
6319 );
6320
6321 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6322 assert_rewrap(
6323 indoc! {"
6324 ˇThis is a very long line of plain text that will be wrapped.
6325 "},
6326 indoc! {"
6327 ˇThis is a very long line of plain text
6328 that will be wrapped.
6329 "},
6330 plaintext_language.clone(),
6331 &mut cx,
6332 );
6333
6334 // Test that non-commented code acts as a paragraph boundary within a selection
6335 assert_rewrap(
6336 indoc! {"
6337 «// This is the first long comment block to be wrapped.
6338 fn my_func(a: u32);
6339 // This is the second long comment block to be wrapped.ˇ»
6340 "},
6341 indoc! {"
6342 «// This is the first long comment block
6343 // to be wrapped.
6344 fn my_func(a: u32);
6345 // This is the second long comment block
6346 // to be wrapped.ˇ»
6347 "},
6348 rust_language,
6349 &mut cx,
6350 );
6351
6352 // Test rewrapping multiple selections, including ones with blank lines or tabs
6353 assert_rewrap(
6354 indoc! {"
6355 «ˇThis is a very long line that will be wrapped.
6356
6357 This is another paragraph in the same selection.»
6358
6359 «\tThis is a very long indented line that will be wrapped.ˇ»
6360 "},
6361 indoc! {"
6362 «ˇThis is a very long line that will be
6363 wrapped.
6364
6365 This is another paragraph in the same
6366 selection.»
6367
6368 «\tThis is a very long indented line
6369 \tthat will be wrapped.ˇ»
6370 "},
6371 plaintext_language,
6372 &mut cx,
6373 );
6374
6375 // Test that an empty comment line acts as a paragraph boundary
6376 assert_rewrap(
6377 indoc! {"
6378 // ˇThis is a long comment that will be wrapped.
6379 //
6380 // And this is another long comment that will also be wrapped.ˇ
6381 "},
6382 indoc! {"
6383 // ˇThis is a long comment that will be
6384 // wrapped.
6385 //
6386 // And this is another long comment that
6387 // will also be wrapped.ˇ
6388 "},
6389 cpp_language,
6390 &mut cx,
6391 );
6392
6393 #[track_caller]
6394 fn assert_rewrap(
6395 unwrapped_text: &str,
6396 wrapped_text: &str,
6397 language: Arc<Language>,
6398 cx: &mut EditorTestContext,
6399 ) {
6400 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6401 cx.set_state(unwrapped_text);
6402 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6403 cx.assert_editor_state(wrapped_text);
6404 }
6405}
6406
6407#[gpui::test]
6408async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6409 init_test(cx, |settings| {
6410 settings.languages.0.extend([(
6411 "Rust".into(),
6412 LanguageSettingsContent {
6413 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6414 preferred_line_length: Some(40),
6415 ..Default::default()
6416 },
6417 )])
6418 });
6419
6420 let mut cx = EditorTestContext::new(cx).await;
6421
6422 let rust_lang = Arc::new(
6423 Language::new(
6424 LanguageConfig {
6425 name: "Rust".into(),
6426 line_comments: vec!["// ".into()],
6427 block_comment: Some(BlockCommentConfig {
6428 start: "/*".into(),
6429 end: "*/".into(),
6430 prefix: "* ".into(),
6431 tab_size: 1,
6432 }),
6433 documentation_comment: Some(BlockCommentConfig {
6434 start: "/**".into(),
6435 end: "*/".into(),
6436 prefix: "* ".into(),
6437 tab_size: 1,
6438 }),
6439
6440 ..LanguageConfig::default()
6441 },
6442 Some(tree_sitter_rust::LANGUAGE.into()),
6443 )
6444 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6445 .unwrap(),
6446 );
6447
6448 // regular block comment
6449 assert_rewrap(
6450 indoc! {"
6451 /*
6452 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6453 */
6454 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6455 "},
6456 indoc! {"
6457 /*
6458 *ˇ Lorem ipsum dolor sit amet,
6459 * consectetur adipiscing elit.
6460 */
6461 /*
6462 *ˇ Lorem ipsum dolor sit amet,
6463 * consectetur adipiscing elit.
6464 */
6465 "},
6466 rust_lang.clone(),
6467 &mut cx,
6468 );
6469
6470 // indent is respected
6471 assert_rewrap(
6472 indoc! {"
6473 {}
6474 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6475 "},
6476 indoc! {"
6477 {}
6478 /*
6479 *ˇ Lorem ipsum dolor sit amet,
6480 * consectetur adipiscing elit.
6481 */
6482 "},
6483 rust_lang.clone(),
6484 &mut cx,
6485 );
6486
6487 // short block comments with inline delimiters
6488 assert_rewrap(
6489 indoc! {"
6490 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6491 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6492 */
6493 /*
6494 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6495 "},
6496 indoc! {"
6497 /*
6498 *ˇ Lorem ipsum dolor sit amet,
6499 * consectetur adipiscing elit.
6500 */
6501 /*
6502 *ˇ Lorem ipsum dolor sit amet,
6503 * consectetur adipiscing elit.
6504 */
6505 /*
6506 *ˇ Lorem ipsum dolor sit amet,
6507 * consectetur adipiscing elit.
6508 */
6509 "},
6510 rust_lang.clone(),
6511 &mut cx,
6512 );
6513
6514 // multiline block comment with inline start/end delimiters
6515 assert_rewrap(
6516 indoc! {"
6517 /*ˇ Lorem ipsum dolor sit amet,
6518 * consectetur adipiscing elit. */
6519 "},
6520 indoc! {"
6521 /*
6522 *ˇ Lorem ipsum dolor sit amet,
6523 * consectetur adipiscing elit.
6524 */
6525 "},
6526 rust_lang.clone(),
6527 &mut cx,
6528 );
6529
6530 // block comment rewrap still respects paragraph bounds
6531 assert_rewrap(
6532 indoc! {"
6533 /*
6534 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6535 *
6536 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6537 */
6538 "},
6539 indoc! {"
6540 /*
6541 *ˇ Lorem ipsum dolor sit amet,
6542 * consectetur adipiscing elit.
6543 *
6544 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6545 */
6546 "},
6547 rust_lang.clone(),
6548 &mut cx,
6549 );
6550
6551 // documentation comments
6552 assert_rewrap(
6553 indoc! {"
6554 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6555 /**
6556 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6557 */
6558 "},
6559 indoc! {"
6560 /**
6561 *ˇ Lorem ipsum dolor sit amet,
6562 * consectetur adipiscing elit.
6563 */
6564 /**
6565 *ˇ Lorem ipsum dolor sit amet,
6566 * consectetur adipiscing elit.
6567 */
6568 "},
6569 rust_lang.clone(),
6570 &mut cx,
6571 );
6572
6573 // different, adjacent comments
6574 assert_rewrap(
6575 indoc! {"
6576 /**
6577 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6578 */
6579 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6580 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6581 "},
6582 indoc! {"
6583 /**
6584 *ˇ Lorem ipsum dolor sit amet,
6585 * consectetur adipiscing elit.
6586 */
6587 /*
6588 *ˇ Lorem ipsum dolor sit amet,
6589 * consectetur adipiscing elit.
6590 */
6591 //ˇ Lorem ipsum dolor sit amet,
6592 // consectetur adipiscing elit.
6593 "},
6594 rust_lang.clone(),
6595 &mut cx,
6596 );
6597
6598 // selection w/ single short block comment
6599 assert_rewrap(
6600 indoc! {"
6601 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6602 "},
6603 indoc! {"
6604 «/*
6605 * Lorem ipsum dolor sit amet,
6606 * consectetur adipiscing elit.
6607 */ˇ»
6608 "},
6609 rust_lang.clone(),
6610 &mut cx,
6611 );
6612
6613 // rewrapping a single comment w/ abutting comments
6614 assert_rewrap(
6615 indoc! {"
6616 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6617 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6618 "},
6619 indoc! {"
6620 /*
6621 * ˇLorem ipsum dolor sit amet,
6622 * consectetur adipiscing elit.
6623 */
6624 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6625 "},
6626 rust_lang.clone(),
6627 &mut cx,
6628 );
6629
6630 // selection w/ non-abutting short block comments
6631 assert_rewrap(
6632 indoc! {"
6633 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6634
6635 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6636 "},
6637 indoc! {"
6638 «/*
6639 * Lorem ipsum dolor sit amet,
6640 * consectetur adipiscing elit.
6641 */
6642
6643 /*
6644 * Lorem ipsum dolor sit amet,
6645 * consectetur adipiscing elit.
6646 */ˇ»
6647 "},
6648 rust_lang.clone(),
6649 &mut cx,
6650 );
6651
6652 // selection of multiline block comments
6653 assert_rewrap(
6654 indoc! {"
6655 «/* Lorem ipsum dolor sit amet,
6656 * consectetur adipiscing elit. */ˇ»
6657 "},
6658 indoc! {"
6659 «/*
6660 * Lorem ipsum dolor sit amet,
6661 * consectetur adipiscing elit.
6662 */ˇ»
6663 "},
6664 rust_lang.clone(),
6665 &mut cx,
6666 );
6667
6668 // partial selection of multiline block comments
6669 assert_rewrap(
6670 indoc! {"
6671 «/* Lorem ipsum dolor sit amet,ˇ»
6672 * consectetur adipiscing elit. */
6673 /* Lorem ipsum dolor sit amet,
6674 «* consectetur adipiscing elit. */ˇ»
6675 "},
6676 indoc! {"
6677 «/*
6678 * Lorem ipsum dolor sit amet,ˇ»
6679 * consectetur adipiscing elit. */
6680 /* Lorem ipsum dolor sit amet,
6681 «* consectetur adipiscing elit.
6682 */ˇ»
6683 "},
6684 rust_lang.clone(),
6685 &mut cx,
6686 );
6687
6688 // selection w/ abutting short block comments
6689 // TODO: should not be combined; should rewrap as 2 comments
6690 assert_rewrap(
6691 indoc! {"
6692 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6693 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6694 "},
6695 // desired behavior:
6696 // indoc! {"
6697 // «/*
6698 // * Lorem ipsum dolor sit amet,
6699 // * consectetur adipiscing elit.
6700 // */
6701 // /*
6702 // * Lorem ipsum dolor sit amet,
6703 // * consectetur adipiscing elit.
6704 // */ˇ»
6705 // "},
6706 // actual behaviour:
6707 indoc! {"
6708 «/*
6709 * Lorem ipsum dolor sit amet,
6710 * consectetur adipiscing elit. Lorem
6711 * ipsum dolor sit amet, consectetur
6712 * adipiscing elit.
6713 */ˇ»
6714 "},
6715 rust_lang.clone(),
6716 &mut cx,
6717 );
6718
6719 // TODO: same as above, but with delimiters on separate line
6720 // assert_rewrap(
6721 // indoc! {"
6722 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6723 // */
6724 // /*
6725 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6726 // "},
6727 // // desired:
6728 // // indoc! {"
6729 // // «/*
6730 // // * Lorem ipsum dolor sit amet,
6731 // // * consectetur adipiscing elit.
6732 // // */
6733 // // /*
6734 // // * Lorem ipsum dolor sit amet,
6735 // // * consectetur adipiscing elit.
6736 // // */ˇ»
6737 // // "},
6738 // // actual: (but with trailing w/s on the empty lines)
6739 // indoc! {"
6740 // «/*
6741 // * Lorem ipsum dolor sit amet,
6742 // * consectetur adipiscing elit.
6743 // *
6744 // */
6745 // /*
6746 // *
6747 // * Lorem ipsum dolor sit amet,
6748 // * consectetur adipiscing elit.
6749 // */ˇ»
6750 // "},
6751 // rust_lang.clone(),
6752 // &mut cx,
6753 // );
6754
6755 // TODO these are unhandled edge cases; not correct, just documenting known issues
6756 assert_rewrap(
6757 indoc! {"
6758 /*
6759 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6760 */
6761 /*
6762 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6763 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6764 "},
6765 // desired:
6766 // indoc! {"
6767 // /*
6768 // *ˇ Lorem ipsum dolor sit amet,
6769 // * consectetur adipiscing elit.
6770 // */
6771 // /*
6772 // *ˇ Lorem ipsum dolor sit amet,
6773 // * consectetur adipiscing elit.
6774 // */
6775 // /*
6776 // *ˇ Lorem ipsum dolor sit amet
6777 // */ /* consectetur adipiscing elit. */
6778 // "},
6779 // actual:
6780 indoc! {"
6781 /*
6782 //ˇ Lorem ipsum dolor sit amet,
6783 // consectetur adipiscing elit.
6784 */
6785 /*
6786 * //ˇ Lorem ipsum dolor sit amet,
6787 * consectetur adipiscing elit.
6788 */
6789 /*
6790 *ˇ Lorem ipsum dolor sit amet */ /*
6791 * consectetur adipiscing elit.
6792 */
6793 "},
6794 rust_lang,
6795 &mut cx,
6796 );
6797
6798 #[track_caller]
6799 fn assert_rewrap(
6800 unwrapped_text: &str,
6801 wrapped_text: &str,
6802 language: Arc<Language>,
6803 cx: &mut EditorTestContext,
6804 ) {
6805 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6806 cx.set_state(unwrapped_text);
6807 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6808 cx.assert_editor_state(wrapped_text);
6809 }
6810}
6811
6812#[gpui::test]
6813async fn test_hard_wrap(cx: &mut TestAppContext) {
6814 init_test(cx, |_| {});
6815 let mut cx = EditorTestContext::new(cx).await;
6816
6817 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6818 cx.update_editor(|editor, _, cx| {
6819 editor.set_hard_wrap(Some(14), cx);
6820 });
6821
6822 cx.set_state(indoc!(
6823 "
6824 one two three ˇ
6825 "
6826 ));
6827 cx.simulate_input("four");
6828 cx.run_until_parked();
6829
6830 cx.assert_editor_state(indoc!(
6831 "
6832 one two three
6833 fourˇ
6834 "
6835 ));
6836
6837 cx.update_editor(|editor, window, cx| {
6838 editor.newline(&Default::default(), window, cx);
6839 });
6840 cx.run_until_parked();
6841 cx.assert_editor_state(indoc!(
6842 "
6843 one two three
6844 four
6845 ˇ
6846 "
6847 ));
6848
6849 cx.simulate_input("five");
6850 cx.run_until_parked();
6851 cx.assert_editor_state(indoc!(
6852 "
6853 one two three
6854 four
6855 fiveˇ
6856 "
6857 ));
6858
6859 cx.update_editor(|editor, window, cx| {
6860 editor.newline(&Default::default(), window, cx);
6861 });
6862 cx.run_until_parked();
6863 cx.simulate_input("# ");
6864 cx.run_until_parked();
6865 cx.assert_editor_state(indoc!(
6866 "
6867 one two three
6868 four
6869 five
6870 # ˇ
6871 "
6872 ));
6873
6874 cx.update_editor(|editor, window, cx| {
6875 editor.newline(&Default::default(), window, cx);
6876 });
6877 cx.run_until_parked();
6878 cx.assert_editor_state(indoc!(
6879 "
6880 one two three
6881 four
6882 five
6883 #\x20
6884 #ˇ
6885 "
6886 ));
6887
6888 cx.simulate_input(" 6");
6889 cx.run_until_parked();
6890 cx.assert_editor_state(indoc!(
6891 "
6892 one two three
6893 four
6894 five
6895 #
6896 # 6ˇ
6897 "
6898 ));
6899}
6900
6901#[gpui::test]
6902async fn test_cut_line_ends(cx: &mut TestAppContext) {
6903 init_test(cx, |_| {});
6904
6905 let mut cx = EditorTestContext::new(cx).await;
6906
6907 cx.set_state(indoc! {"The quick brownˇ"});
6908 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6909 cx.assert_editor_state(indoc! {"The quick brownˇ"});
6910
6911 cx.set_state(indoc! {"The emacs foxˇ"});
6912 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6913 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
6914
6915 cx.set_state(indoc! {"
6916 The quick« brownˇ»
6917 fox jumps overˇ
6918 the lazy dog"});
6919 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6920 cx.assert_editor_state(indoc! {"
6921 The quickˇ
6922 ˇthe lazy dog"});
6923
6924 cx.set_state(indoc! {"
6925 The quick« brownˇ»
6926 fox jumps overˇ
6927 the lazy dog"});
6928 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6929 cx.assert_editor_state(indoc! {"
6930 The quickˇ
6931 fox jumps overˇthe lazy dog"});
6932
6933 cx.set_state(indoc! {"
6934 The quick« brownˇ»
6935 fox jumps overˇ
6936 the lazy dog"});
6937 cx.update_editor(|e, window, cx| {
6938 e.cut_to_end_of_line(
6939 &CutToEndOfLine {
6940 stop_at_newlines: true,
6941 },
6942 window,
6943 cx,
6944 )
6945 });
6946 cx.assert_editor_state(indoc! {"
6947 The quickˇ
6948 fox jumps overˇ
6949 the lazy dog"});
6950
6951 cx.set_state(indoc! {"
6952 The quick« brownˇ»
6953 fox jumps overˇ
6954 the lazy dog"});
6955 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6956 cx.assert_editor_state(indoc! {"
6957 The quickˇ
6958 fox jumps overˇthe lazy dog"});
6959}
6960
6961#[gpui::test]
6962async fn test_clipboard(cx: &mut TestAppContext) {
6963 init_test(cx, |_| {});
6964
6965 let mut cx = EditorTestContext::new(cx).await;
6966
6967 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
6968 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6969 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
6970
6971 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
6972 cx.set_state("two ˇfour ˇsix ˇ");
6973 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6974 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
6975
6976 // Paste again but with only two cursors. Since the number of cursors doesn't
6977 // match the number of slices in the clipboard, the entire clipboard text
6978 // is pasted at each cursor.
6979 cx.set_state("ˇtwo one✅ four three six five ˇ");
6980 cx.update_editor(|e, window, cx| {
6981 e.handle_input("( ", window, cx);
6982 e.paste(&Paste, window, cx);
6983 e.handle_input(") ", window, cx);
6984 });
6985 cx.assert_editor_state(
6986 &([
6987 "( one✅ ",
6988 "three ",
6989 "five ) ˇtwo one✅ four three six five ( one✅ ",
6990 "three ",
6991 "five ) ˇ",
6992 ]
6993 .join("\n")),
6994 );
6995
6996 // Cut with three selections, one of which is full-line.
6997 cx.set_state(indoc! {"
6998 1«2ˇ»3
6999 4ˇ567
7000 «8ˇ»9"});
7001 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7002 cx.assert_editor_state(indoc! {"
7003 1ˇ3
7004 ˇ9"});
7005
7006 // Paste with three selections, noticing how the copied selection that was full-line
7007 // gets inserted before the second cursor.
7008 cx.set_state(indoc! {"
7009 1ˇ3
7010 9ˇ
7011 «oˇ»ne"});
7012 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7013 cx.assert_editor_state(indoc! {"
7014 12ˇ3
7015 4567
7016 9ˇ
7017 8ˇne"});
7018
7019 // Copy with a single cursor only, which writes the whole line into the clipboard.
7020 cx.set_state(indoc! {"
7021 The quick brown
7022 fox juˇmps over
7023 the lazy dog"});
7024 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7025 assert_eq!(
7026 cx.read_from_clipboard()
7027 .and_then(|item| item.text().as_deref().map(str::to_string)),
7028 Some("fox jumps over\n".to_string())
7029 );
7030
7031 // Paste with three selections, noticing how the copied full-line selection is inserted
7032 // before the empty selections but replaces the selection that is non-empty.
7033 cx.set_state(indoc! {"
7034 Tˇhe quick brown
7035 «foˇ»x jumps over
7036 tˇhe lazy dog"});
7037 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7038 cx.assert_editor_state(indoc! {"
7039 fox jumps over
7040 Tˇhe quick brown
7041 fox jumps over
7042 ˇx jumps over
7043 fox jumps over
7044 tˇhe lazy dog"});
7045}
7046
7047#[gpui::test]
7048async fn test_copy_trim(cx: &mut TestAppContext) {
7049 init_test(cx, |_| {});
7050
7051 let mut cx = EditorTestContext::new(cx).await;
7052 cx.set_state(
7053 r#" «for selection in selections.iter() {
7054 let mut start = selection.start;
7055 let mut end = selection.end;
7056 let is_entire_line = selection.is_empty();
7057 if is_entire_line {
7058 start = Point::new(start.row, 0);ˇ»
7059 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7060 }
7061 "#,
7062 );
7063 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7064 assert_eq!(
7065 cx.read_from_clipboard()
7066 .and_then(|item| item.text().as_deref().map(str::to_string)),
7067 Some(
7068 "for selection in selections.iter() {
7069 let mut start = selection.start;
7070 let mut end = selection.end;
7071 let is_entire_line = selection.is_empty();
7072 if is_entire_line {
7073 start = Point::new(start.row, 0);"
7074 .to_string()
7075 ),
7076 "Regular copying preserves all indentation selected",
7077 );
7078 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7079 assert_eq!(
7080 cx.read_from_clipboard()
7081 .and_then(|item| item.text().as_deref().map(str::to_string)),
7082 Some(
7083 "for selection in selections.iter() {
7084let mut start = selection.start;
7085let mut end = selection.end;
7086let is_entire_line = selection.is_empty();
7087if is_entire_line {
7088 start = Point::new(start.row, 0);"
7089 .to_string()
7090 ),
7091 "Copying with stripping should strip all leading whitespaces"
7092 );
7093
7094 cx.set_state(
7095 r#" « for selection in selections.iter() {
7096 let mut start = selection.start;
7097 let mut end = selection.end;
7098 let is_entire_line = selection.is_empty();
7099 if is_entire_line {
7100 start = Point::new(start.row, 0);ˇ»
7101 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7102 }
7103 "#,
7104 );
7105 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7106 assert_eq!(
7107 cx.read_from_clipboard()
7108 .and_then(|item| item.text().as_deref().map(str::to_string)),
7109 Some(
7110 " for selection in selections.iter() {
7111 let mut start = selection.start;
7112 let mut end = selection.end;
7113 let is_entire_line = selection.is_empty();
7114 if is_entire_line {
7115 start = Point::new(start.row, 0);"
7116 .to_string()
7117 ),
7118 "Regular copying preserves all indentation selected",
7119 );
7120 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7121 assert_eq!(
7122 cx.read_from_clipboard()
7123 .and_then(|item| item.text().as_deref().map(str::to_string)),
7124 Some(
7125 "for selection in selections.iter() {
7126let mut start = selection.start;
7127let mut end = selection.end;
7128let is_entire_line = selection.is_empty();
7129if is_entire_line {
7130 start = Point::new(start.row, 0);"
7131 .to_string()
7132 ),
7133 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7134 );
7135
7136 cx.set_state(
7137 r#" «ˇ for selection in selections.iter() {
7138 let mut start = selection.start;
7139 let mut end = selection.end;
7140 let is_entire_line = selection.is_empty();
7141 if is_entire_line {
7142 start = Point::new(start.row, 0);»
7143 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7144 }
7145 "#,
7146 );
7147 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7148 assert_eq!(
7149 cx.read_from_clipboard()
7150 .and_then(|item| item.text().as_deref().map(str::to_string)),
7151 Some(
7152 " for selection in selections.iter() {
7153 let mut start = selection.start;
7154 let mut end = selection.end;
7155 let is_entire_line = selection.is_empty();
7156 if is_entire_line {
7157 start = Point::new(start.row, 0);"
7158 .to_string()
7159 ),
7160 "Regular copying for reverse selection works the same",
7161 );
7162 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7163 assert_eq!(
7164 cx.read_from_clipboard()
7165 .and_then(|item| item.text().as_deref().map(str::to_string)),
7166 Some(
7167 "for selection in selections.iter() {
7168let mut start = selection.start;
7169let mut end = selection.end;
7170let is_entire_line = selection.is_empty();
7171if is_entire_line {
7172 start = Point::new(start.row, 0);"
7173 .to_string()
7174 ),
7175 "Copying with stripping for reverse selection works the same"
7176 );
7177
7178 cx.set_state(
7179 r#" for selection «in selections.iter() {
7180 let mut start = selection.start;
7181 let mut end = selection.end;
7182 let is_entire_line = selection.is_empty();
7183 if is_entire_line {
7184 start = Point::new(start.row, 0);ˇ»
7185 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7186 }
7187 "#,
7188 );
7189 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7190 assert_eq!(
7191 cx.read_from_clipboard()
7192 .and_then(|item| item.text().as_deref().map(str::to_string)),
7193 Some(
7194 "in selections.iter() {
7195 let mut start = selection.start;
7196 let mut end = selection.end;
7197 let is_entire_line = selection.is_empty();
7198 if is_entire_line {
7199 start = Point::new(start.row, 0);"
7200 .to_string()
7201 ),
7202 "When selecting past the indent, the copying works as usual",
7203 );
7204 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7205 assert_eq!(
7206 cx.read_from_clipboard()
7207 .and_then(|item| item.text().as_deref().map(str::to_string)),
7208 Some(
7209 "in selections.iter() {
7210 let mut start = selection.start;
7211 let mut end = selection.end;
7212 let is_entire_line = selection.is_empty();
7213 if is_entire_line {
7214 start = Point::new(start.row, 0);"
7215 .to_string()
7216 ),
7217 "When selecting past the indent, nothing is trimmed"
7218 );
7219
7220 cx.set_state(
7221 r#" «for selection in selections.iter() {
7222 let mut start = selection.start;
7223
7224 let mut end = selection.end;
7225 let is_entire_line = selection.is_empty();
7226 if is_entire_line {
7227 start = Point::new(start.row, 0);
7228ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7229 }
7230 "#,
7231 );
7232 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7233 assert_eq!(
7234 cx.read_from_clipboard()
7235 .and_then(|item| item.text().as_deref().map(str::to_string)),
7236 Some(
7237 "for selection in selections.iter() {
7238let mut start = selection.start;
7239
7240let mut end = selection.end;
7241let is_entire_line = selection.is_empty();
7242if is_entire_line {
7243 start = Point::new(start.row, 0);
7244"
7245 .to_string()
7246 ),
7247 "Copying with stripping should ignore empty lines"
7248 );
7249}
7250
7251#[gpui::test]
7252async fn test_paste_multiline(cx: &mut TestAppContext) {
7253 init_test(cx, |_| {});
7254
7255 let mut cx = EditorTestContext::new(cx).await;
7256 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7257
7258 // Cut an indented block, without the leading whitespace.
7259 cx.set_state(indoc! {"
7260 const a: B = (
7261 c(),
7262 «d(
7263 e,
7264 f
7265 )ˇ»
7266 );
7267 "});
7268 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7269 cx.assert_editor_state(indoc! {"
7270 const a: B = (
7271 c(),
7272 ˇ
7273 );
7274 "});
7275
7276 // Paste it at the same position.
7277 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7278 cx.assert_editor_state(indoc! {"
7279 const a: B = (
7280 c(),
7281 d(
7282 e,
7283 f
7284 )ˇ
7285 );
7286 "});
7287
7288 // Paste it at a line with a lower indent level.
7289 cx.set_state(indoc! {"
7290 ˇ
7291 const a: B = (
7292 c(),
7293 );
7294 "});
7295 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7296 cx.assert_editor_state(indoc! {"
7297 d(
7298 e,
7299 f
7300 )ˇ
7301 const a: B = (
7302 c(),
7303 );
7304 "});
7305
7306 // Cut an indented block, with the leading whitespace.
7307 cx.set_state(indoc! {"
7308 const a: B = (
7309 c(),
7310 « d(
7311 e,
7312 f
7313 )
7314 ˇ»);
7315 "});
7316 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7317 cx.assert_editor_state(indoc! {"
7318 const a: B = (
7319 c(),
7320 ˇ);
7321 "});
7322
7323 // Paste it at the same position.
7324 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7325 cx.assert_editor_state(indoc! {"
7326 const a: B = (
7327 c(),
7328 d(
7329 e,
7330 f
7331 )
7332 ˇ);
7333 "});
7334
7335 // Paste it at a line with a higher indent level.
7336 cx.set_state(indoc! {"
7337 const a: B = (
7338 c(),
7339 d(
7340 e,
7341 fˇ
7342 )
7343 );
7344 "});
7345 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7346 cx.assert_editor_state(indoc! {"
7347 const a: B = (
7348 c(),
7349 d(
7350 e,
7351 f d(
7352 e,
7353 f
7354 )
7355 ˇ
7356 )
7357 );
7358 "});
7359
7360 // Copy an indented block, starting mid-line
7361 cx.set_state(indoc! {"
7362 const a: B = (
7363 c(),
7364 somethin«g(
7365 e,
7366 f
7367 )ˇ»
7368 );
7369 "});
7370 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7371
7372 // Paste it on a line with a lower indent level
7373 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7374 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7375 cx.assert_editor_state(indoc! {"
7376 const a: B = (
7377 c(),
7378 something(
7379 e,
7380 f
7381 )
7382 );
7383 g(
7384 e,
7385 f
7386 )ˇ"});
7387}
7388
7389#[gpui::test]
7390async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7391 init_test(cx, |_| {});
7392
7393 cx.write_to_clipboard(ClipboardItem::new_string(
7394 " d(\n e\n );\n".into(),
7395 ));
7396
7397 let mut cx = EditorTestContext::new(cx).await;
7398 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7399
7400 cx.set_state(indoc! {"
7401 fn a() {
7402 b();
7403 if c() {
7404 ˇ
7405 }
7406 }
7407 "});
7408
7409 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7410 cx.assert_editor_state(indoc! {"
7411 fn a() {
7412 b();
7413 if c() {
7414 d(
7415 e
7416 );
7417 ˇ
7418 }
7419 }
7420 "});
7421
7422 cx.set_state(indoc! {"
7423 fn a() {
7424 b();
7425 ˇ
7426 }
7427 "});
7428
7429 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7430 cx.assert_editor_state(indoc! {"
7431 fn a() {
7432 b();
7433 d(
7434 e
7435 );
7436 ˇ
7437 }
7438 "});
7439}
7440
7441#[gpui::test]
7442fn test_select_all(cx: &mut TestAppContext) {
7443 init_test(cx, |_| {});
7444
7445 let editor = cx.add_window(|window, cx| {
7446 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7447 build_editor(buffer, window, cx)
7448 });
7449 _ = editor.update(cx, |editor, window, cx| {
7450 editor.select_all(&SelectAll, window, cx);
7451 assert_eq!(
7452 editor.selections.display_ranges(cx),
7453 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7454 );
7455 });
7456}
7457
7458#[gpui::test]
7459fn test_select_line(cx: &mut TestAppContext) {
7460 init_test(cx, |_| {});
7461
7462 let editor = cx.add_window(|window, cx| {
7463 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7464 build_editor(buffer, window, cx)
7465 });
7466 _ = editor.update(cx, |editor, window, cx| {
7467 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7468 s.select_display_ranges([
7469 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7470 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7471 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7472 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7473 ])
7474 });
7475 editor.select_line(&SelectLine, window, cx);
7476 assert_eq!(
7477 editor.selections.display_ranges(cx),
7478 vec![
7479 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7480 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7481 ]
7482 );
7483 });
7484
7485 _ = editor.update(cx, |editor, window, cx| {
7486 editor.select_line(&SelectLine, window, cx);
7487 assert_eq!(
7488 editor.selections.display_ranges(cx),
7489 vec![
7490 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7491 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7492 ]
7493 );
7494 });
7495
7496 _ = editor.update(cx, |editor, window, cx| {
7497 editor.select_line(&SelectLine, window, cx);
7498 assert_eq!(
7499 editor.selections.display_ranges(cx),
7500 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7501 );
7502 });
7503}
7504
7505#[gpui::test]
7506async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7507 init_test(cx, |_| {});
7508 let mut cx = EditorTestContext::new(cx).await;
7509
7510 #[track_caller]
7511 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7512 cx.set_state(initial_state);
7513 cx.update_editor(|e, window, cx| {
7514 e.split_selection_into_lines(&Default::default(), window, cx)
7515 });
7516 cx.assert_editor_state(expected_state);
7517 }
7518
7519 // Selection starts and ends at the middle of lines, left-to-right
7520 test(
7521 &mut cx,
7522 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7523 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7524 );
7525 // Same thing, right-to-left
7526 test(
7527 &mut cx,
7528 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7529 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7530 );
7531
7532 // Whole buffer, left-to-right, last line *doesn't* end with newline
7533 test(
7534 &mut cx,
7535 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7536 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7537 );
7538 // Same thing, right-to-left
7539 test(
7540 &mut cx,
7541 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7542 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7543 );
7544
7545 // Whole buffer, left-to-right, last line ends with newline
7546 test(
7547 &mut cx,
7548 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7549 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7550 );
7551 // Same thing, right-to-left
7552 test(
7553 &mut cx,
7554 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7555 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7556 );
7557
7558 // Starts at the end of a line, ends at the start of another
7559 test(
7560 &mut cx,
7561 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7562 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7563 );
7564}
7565
7566#[gpui::test]
7567async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7568 init_test(cx, |_| {});
7569
7570 let editor = cx.add_window(|window, cx| {
7571 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7572 build_editor(buffer, window, cx)
7573 });
7574
7575 // setup
7576 _ = editor.update(cx, |editor, window, cx| {
7577 editor.fold_creases(
7578 vec![
7579 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7580 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7581 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7582 ],
7583 true,
7584 window,
7585 cx,
7586 );
7587 assert_eq!(
7588 editor.display_text(cx),
7589 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7590 );
7591 });
7592
7593 _ = editor.update(cx, |editor, window, cx| {
7594 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7595 s.select_display_ranges([
7596 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7597 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7598 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7599 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7600 ])
7601 });
7602 editor.split_selection_into_lines(&Default::default(), window, cx);
7603 assert_eq!(
7604 editor.display_text(cx),
7605 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7606 );
7607 });
7608 EditorTestContext::for_editor(editor, cx)
7609 .await
7610 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7611
7612 _ = editor.update(cx, |editor, window, cx| {
7613 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7614 s.select_display_ranges([
7615 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7616 ])
7617 });
7618 editor.split_selection_into_lines(&Default::default(), window, cx);
7619 assert_eq!(
7620 editor.display_text(cx),
7621 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7622 );
7623 assert_eq!(
7624 editor.selections.display_ranges(cx),
7625 [
7626 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7627 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7628 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7629 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7630 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7631 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7632 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7633 ]
7634 );
7635 });
7636 EditorTestContext::for_editor(editor, cx)
7637 .await
7638 .assert_editor_state(
7639 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7640 );
7641}
7642
7643#[gpui::test]
7644async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7645 init_test(cx, |_| {});
7646
7647 let mut cx = EditorTestContext::new(cx).await;
7648
7649 cx.set_state(indoc!(
7650 r#"abc
7651 defˇghi
7652
7653 jk
7654 nlmo
7655 "#
7656 ));
7657
7658 cx.update_editor(|editor, window, cx| {
7659 editor.add_selection_above(&Default::default(), window, cx);
7660 });
7661
7662 cx.assert_editor_state(indoc!(
7663 r#"abcˇ
7664 defˇghi
7665
7666 jk
7667 nlmo
7668 "#
7669 ));
7670
7671 cx.update_editor(|editor, window, cx| {
7672 editor.add_selection_above(&Default::default(), window, cx);
7673 });
7674
7675 cx.assert_editor_state(indoc!(
7676 r#"abcˇ
7677 defˇghi
7678
7679 jk
7680 nlmo
7681 "#
7682 ));
7683
7684 cx.update_editor(|editor, window, cx| {
7685 editor.add_selection_below(&Default::default(), window, cx);
7686 });
7687
7688 cx.assert_editor_state(indoc!(
7689 r#"abc
7690 defˇghi
7691
7692 jk
7693 nlmo
7694 "#
7695 ));
7696
7697 cx.update_editor(|editor, window, cx| {
7698 editor.undo_selection(&Default::default(), window, cx);
7699 });
7700
7701 cx.assert_editor_state(indoc!(
7702 r#"abcˇ
7703 defˇghi
7704
7705 jk
7706 nlmo
7707 "#
7708 ));
7709
7710 cx.update_editor(|editor, window, cx| {
7711 editor.redo_selection(&Default::default(), window, cx);
7712 });
7713
7714 cx.assert_editor_state(indoc!(
7715 r#"abc
7716 defˇghi
7717
7718 jk
7719 nlmo
7720 "#
7721 ));
7722
7723 cx.update_editor(|editor, window, cx| {
7724 editor.add_selection_below(&Default::default(), window, cx);
7725 });
7726
7727 cx.assert_editor_state(indoc!(
7728 r#"abc
7729 defˇghi
7730 ˇ
7731 jk
7732 nlmo
7733 "#
7734 ));
7735
7736 cx.update_editor(|editor, window, cx| {
7737 editor.add_selection_below(&Default::default(), window, cx);
7738 });
7739
7740 cx.assert_editor_state(indoc!(
7741 r#"abc
7742 defˇghi
7743 ˇ
7744 jkˇ
7745 nlmo
7746 "#
7747 ));
7748
7749 cx.update_editor(|editor, window, cx| {
7750 editor.add_selection_below(&Default::default(), window, cx);
7751 });
7752
7753 cx.assert_editor_state(indoc!(
7754 r#"abc
7755 defˇghi
7756 ˇ
7757 jkˇ
7758 nlmˇo
7759 "#
7760 ));
7761
7762 cx.update_editor(|editor, window, cx| {
7763 editor.add_selection_below(&Default::default(), window, cx);
7764 });
7765
7766 cx.assert_editor_state(indoc!(
7767 r#"abc
7768 defˇghi
7769 ˇ
7770 jkˇ
7771 nlmˇo
7772 ˇ"#
7773 ));
7774
7775 // change selections
7776 cx.set_state(indoc!(
7777 r#"abc
7778 def«ˇg»hi
7779
7780 jk
7781 nlmo
7782 "#
7783 ));
7784
7785 cx.update_editor(|editor, window, cx| {
7786 editor.add_selection_below(&Default::default(), window, cx);
7787 });
7788
7789 cx.assert_editor_state(indoc!(
7790 r#"abc
7791 def«ˇg»hi
7792
7793 jk
7794 nlm«ˇo»
7795 "#
7796 ));
7797
7798 cx.update_editor(|editor, window, cx| {
7799 editor.add_selection_below(&Default::default(), window, cx);
7800 });
7801
7802 cx.assert_editor_state(indoc!(
7803 r#"abc
7804 def«ˇg»hi
7805
7806 jk
7807 nlm«ˇo»
7808 "#
7809 ));
7810
7811 cx.update_editor(|editor, window, cx| {
7812 editor.add_selection_above(&Default::default(), window, cx);
7813 });
7814
7815 cx.assert_editor_state(indoc!(
7816 r#"abc
7817 def«ˇg»hi
7818
7819 jk
7820 nlmo
7821 "#
7822 ));
7823
7824 cx.update_editor(|editor, window, cx| {
7825 editor.add_selection_above(&Default::default(), window, cx);
7826 });
7827
7828 cx.assert_editor_state(indoc!(
7829 r#"abc
7830 def«ˇg»hi
7831
7832 jk
7833 nlmo
7834 "#
7835 ));
7836
7837 // Change selections again
7838 cx.set_state(indoc!(
7839 r#"a«bc
7840 defgˇ»hi
7841
7842 jk
7843 nlmo
7844 "#
7845 ));
7846
7847 cx.update_editor(|editor, window, cx| {
7848 editor.add_selection_below(&Default::default(), window, cx);
7849 });
7850
7851 cx.assert_editor_state(indoc!(
7852 r#"a«bcˇ»
7853 d«efgˇ»hi
7854
7855 j«kˇ»
7856 nlmo
7857 "#
7858 ));
7859
7860 cx.update_editor(|editor, window, cx| {
7861 editor.add_selection_below(&Default::default(), window, cx);
7862 });
7863 cx.assert_editor_state(indoc!(
7864 r#"a«bcˇ»
7865 d«efgˇ»hi
7866
7867 j«kˇ»
7868 n«lmoˇ»
7869 "#
7870 ));
7871 cx.update_editor(|editor, window, cx| {
7872 editor.add_selection_above(&Default::default(), window, cx);
7873 });
7874
7875 cx.assert_editor_state(indoc!(
7876 r#"a«bcˇ»
7877 d«efgˇ»hi
7878
7879 j«kˇ»
7880 nlmo
7881 "#
7882 ));
7883
7884 // Change selections again
7885 cx.set_state(indoc!(
7886 r#"abc
7887 d«ˇefghi
7888
7889 jk
7890 nlm»o
7891 "#
7892 ));
7893
7894 cx.update_editor(|editor, window, cx| {
7895 editor.add_selection_above(&Default::default(), window, cx);
7896 });
7897
7898 cx.assert_editor_state(indoc!(
7899 r#"a«ˇbc»
7900 d«ˇef»ghi
7901
7902 j«ˇk»
7903 n«ˇlm»o
7904 "#
7905 ));
7906
7907 cx.update_editor(|editor, window, cx| {
7908 editor.add_selection_below(&Default::default(), window, cx);
7909 });
7910
7911 cx.assert_editor_state(indoc!(
7912 r#"abc
7913 d«ˇef»ghi
7914
7915 j«ˇk»
7916 n«ˇlm»o
7917 "#
7918 ));
7919}
7920
7921#[gpui::test]
7922async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
7923 init_test(cx, |_| {});
7924 let mut cx = EditorTestContext::new(cx).await;
7925
7926 cx.set_state(indoc!(
7927 r#"line onˇe
7928 liˇne two
7929 line three
7930 line four"#
7931 ));
7932
7933 cx.update_editor(|editor, window, cx| {
7934 editor.add_selection_below(&Default::default(), window, cx);
7935 });
7936
7937 // test multiple cursors expand in the same direction
7938 cx.assert_editor_state(indoc!(
7939 r#"line onˇe
7940 liˇne twˇo
7941 liˇne three
7942 line four"#
7943 ));
7944
7945 cx.update_editor(|editor, window, cx| {
7946 editor.add_selection_below(&Default::default(), window, cx);
7947 });
7948
7949 cx.update_editor(|editor, window, cx| {
7950 editor.add_selection_below(&Default::default(), window, cx);
7951 });
7952
7953 // test multiple cursors expand below overflow
7954 cx.assert_editor_state(indoc!(
7955 r#"line onˇe
7956 liˇne twˇo
7957 liˇne thˇree
7958 liˇne foˇur"#
7959 ));
7960
7961 cx.update_editor(|editor, window, cx| {
7962 editor.add_selection_above(&Default::default(), window, cx);
7963 });
7964
7965 // test multiple cursors retrieves back correctly
7966 cx.assert_editor_state(indoc!(
7967 r#"line onˇe
7968 liˇne twˇo
7969 liˇne thˇree
7970 line four"#
7971 ));
7972
7973 cx.update_editor(|editor, window, cx| {
7974 editor.add_selection_above(&Default::default(), window, cx);
7975 });
7976
7977 cx.update_editor(|editor, window, cx| {
7978 editor.add_selection_above(&Default::default(), window, cx);
7979 });
7980
7981 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
7982 cx.assert_editor_state(indoc!(
7983 r#"liˇne onˇe
7984 liˇne two
7985 line three
7986 line four"#
7987 ));
7988
7989 cx.update_editor(|editor, window, cx| {
7990 editor.undo_selection(&Default::default(), window, cx);
7991 });
7992
7993 // test undo
7994 cx.assert_editor_state(indoc!(
7995 r#"line onˇe
7996 liˇne twˇo
7997 line three
7998 line four"#
7999 ));
8000
8001 cx.update_editor(|editor, window, cx| {
8002 editor.redo_selection(&Default::default(), window, cx);
8003 });
8004
8005 // test redo
8006 cx.assert_editor_state(indoc!(
8007 r#"liˇne onˇe
8008 liˇne two
8009 line three
8010 line four"#
8011 ));
8012
8013 cx.set_state(indoc!(
8014 r#"abcd
8015 ef«ghˇ»
8016 ijkl
8017 «mˇ»nop"#
8018 ));
8019
8020 cx.update_editor(|editor, window, cx| {
8021 editor.add_selection_above(&Default::default(), window, cx);
8022 });
8023
8024 // test multiple selections expand in the same direction
8025 cx.assert_editor_state(indoc!(
8026 r#"ab«cdˇ»
8027 ef«ghˇ»
8028 «iˇ»jkl
8029 «mˇ»nop"#
8030 ));
8031
8032 cx.update_editor(|editor, window, cx| {
8033 editor.add_selection_above(&Default::default(), window, cx);
8034 });
8035
8036 // test multiple selection upward overflow
8037 cx.assert_editor_state(indoc!(
8038 r#"ab«cdˇ»
8039 «eˇ»f«ghˇ»
8040 «iˇ»jkl
8041 «mˇ»nop"#
8042 ));
8043
8044 cx.update_editor(|editor, window, cx| {
8045 editor.add_selection_below(&Default::default(), window, cx);
8046 });
8047
8048 // test multiple selection retrieves back correctly
8049 cx.assert_editor_state(indoc!(
8050 r#"abcd
8051 ef«ghˇ»
8052 «iˇ»jkl
8053 «mˇ»nop"#
8054 ));
8055
8056 cx.update_editor(|editor, window, cx| {
8057 editor.add_selection_below(&Default::default(), window, cx);
8058 });
8059
8060 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8061 cx.assert_editor_state(indoc!(
8062 r#"abcd
8063 ef«ghˇ»
8064 ij«klˇ»
8065 «mˇ»nop"#
8066 ));
8067
8068 cx.update_editor(|editor, window, cx| {
8069 editor.undo_selection(&Default::default(), window, cx);
8070 });
8071
8072 // test undo
8073 cx.assert_editor_state(indoc!(
8074 r#"abcd
8075 ef«ghˇ»
8076 «iˇ»jkl
8077 «mˇ»nop"#
8078 ));
8079
8080 cx.update_editor(|editor, window, cx| {
8081 editor.redo_selection(&Default::default(), window, cx);
8082 });
8083
8084 // test redo
8085 cx.assert_editor_state(indoc!(
8086 r#"abcd
8087 ef«ghˇ»
8088 ij«klˇ»
8089 «mˇ»nop"#
8090 ));
8091}
8092
8093#[gpui::test]
8094async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8095 init_test(cx, |_| {});
8096 let mut cx = EditorTestContext::new(cx).await;
8097
8098 cx.set_state(indoc!(
8099 r#"line onˇe
8100 liˇne two
8101 line three
8102 line four"#
8103 ));
8104
8105 cx.update_editor(|editor, window, cx| {
8106 editor.add_selection_below(&Default::default(), window, cx);
8107 editor.add_selection_below(&Default::default(), window, cx);
8108 editor.add_selection_below(&Default::default(), window, cx);
8109 });
8110
8111 // initial state with two multi cursor groups
8112 cx.assert_editor_state(indoc!(
8113 r#"line onˇe
8114 liˇne twˇo
8115 liˇne thˇree
8116 liˇne foˇur"#
8117 ));
8118
8119 // add single cursor in middle - simulate opt click
8120 cx.update_editor(|editor, window, cx| {
8121 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8122 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8123 editor.end_selection(window, cx);
8124 });
8125
8126 cx.assert_editor_state(indoc!(
8127 r#"line onˇe
8128 liˇne twˇo
8129 liˇneˇ thˇree
8130 liˇne foˇur"#
8131 ));
8132
8133 cx.update_editor(|editor, window, cx| {
8134 editor.add_selection_above(&Default::default(), window, cx);
8135 });
8136
8137 // test new added selection expands above and existing selection shrinks
8138 cx.assert_editor_state(indoc!(
8139 r#"line onˇe
8140 liˇneˇ twˇo
8141 liˇneˇ thˇree
8142 line four"#
8143 ));
8144
8145 cx.update_editor(|editor, window, cx| {
8146 editor.add_selection_above(&Default::default(), window, cx);
8147 });
8148
8149 // test new added selection expands above and existing selection shrinks
8150 cx.assert_editor_state(indoc!(
8151 r#"lineˇ onˇe
8152 liˇneˇ twˇo
8153 lineˇ three
8154 line four"#
8155 ));
8156
8157 // intial state with two selection groups
8158 cx.set_state(indoc!(
8159 r#"abcd
8160 ef«ghˇ»
8161 ijkl
8162 «mˇ»nop"#
8163 ));
8164
8165 cx.update_editor(|editor, window, cx| {
8166 editor.add_selection_above(&Default::default(), window, cx);
8167 editor.add_selection_above(&Default::default(), window, cx);
8168 });
8169
8170 cx.assert_editor_state(indoc!(
8171 r#"ab«cdˇ»
8172 «eˇ»f«ghˇ»
8173 «iˇ»jkl
8174 «mˇ»nop"#
8175 ));
8176
8177 // add single selection in middle - simulate opt drag
8178 cx.update_editor(|editor, window, cx| {
8179 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8180 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8181 editor.update_selection(
8182 DisplayPoint::new(DisplayRow(2), 4),
8183 0,
8184 gpui::Point::<f32>::default(),
8185 window,
8186 cx,
8187 );
8188 editor.end_selection(window, cx);
8189 });
8190
8191 cx.assert_editor_state(indoc!(
8192 r#"ab«cdˇ»
8193 «eˇ»f«ghˇ»
8194 «iˇ»jk«lˇ»
8195 «mˇ»nop"#
8196 ));
8197
8198 cx.update_editor(|editor, window, cx| {
8199 editor.add_selection_below(&Default::default(), window, cx);
8200 });
8201
8202 // test new added selection expands below, others shrinks from above
8203 cx.assert_editor_state(indoc!(
8204 r#"abcd
8205 ef«ghˇ»
8206 «iˇ»jk«lˇ»
8207 «mˇ»no«pˇ»"#
8208 ));
8209}
8210
8211#[gpui::test]
8212async fn test_select_next(cx: &mut TestAppContext) {
8213 init_test(cx, |_| {});
8214
8215 let mut cx = EditorTestContext::new(cx).await;
8216 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8217
8218 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8219 .unwrap();
8220 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8221
8222 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8223 .unwrap();
8224 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8225
8226 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8227 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8228
8229 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8230 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8231
8232 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8233 .unwrap();
8234 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8235
8236 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8237 .unwrap();
8238 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8239
8240 // Test selection direction should be preserved
8241 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8242
8243 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8244 .unwrap();
8245 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8246}
8247
8248#[gpui::test]
8249async fn test_select_all_matches(cx: &mut TestAppContext) {
8250 init_test(cx, |_| {});
8251
8252 let mut cx = EditorTestContext::new(cx).await;
8253
8254 // Test caret-only selections
8255 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8256 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8257 .unwrap();
8258 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8259
8260 // Test left-to-right selections
8261 cx.set_state("abc\n«abcˇ»\nabc");
8262 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8263 .unwrap();
8264 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8265
8266 // Test right-to-left selections
8267 cx.set_state("abc\n«ˇabc»\nabc");
8268 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8269 .unwrap();
8270 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8271
8272 // Test selecting whitespace with caret selection
8273 cx.set_state("abc\nˇ abc\nabc");
8274 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8275 .unwrap();
8276 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8277
8278 // Test selecting whitespace with left-to-right selection
8279 cx.set_state("abc\n«ˇ »abc\nabc");
8280 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8281 .unwrap();
8282 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8283
8284 // Test no matches with right-to-left selection
8285 cx.set_state("abc\n« ˇ»abc\nabc");
8286 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8287 .unwrap();
8288 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8289
8290 // Test with a single word and clip_at_line_ends=true (#29823)
8291 cx.set_state("aˇbc");
8292 cx.update_editor(|e, window, cx| {
8293 e.set_clip_at_line_ends(true, cx);
8294 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8295 e.set_clip_at_line_ends(false, cx);
8296 });
8297 cx.assert_editor_state("«abcˇ»");
8298}
8299
8300#[gpui::test]
8301async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8302 init_test(cx, |_| {});
8303
8304 let mut cx = EditorTestContext::new(cx).await;
8305
8306 let large_body_1 = "\nd".repeat(200);
8307 let large_body_2 = "\ne".repeat(200);
8308
8309 cx.set_state(&format!(
8310 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8311 ));
8312 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8313 let scroll_position = editor.scroll_position(cx);
8314 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8315 scroll_position
8316 });
8317
8318 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8319 .unwrap();
8320 cx.assert_editor_state(&format!(
8321 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8322 ));
8323 let scroll_position_after_selection =
8324 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8325 assert_eq!(
8326 initial_scroll_position, scroll_position_after_selection,
8327 "Scroll position should not change after selecting all matches"
8328 );
8329}
8330
8331#[gpui::test]
8332async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8333 init_test(cx, |_| {});
8334
8335 let mut cx = EditorLspTestContext::new_rust(
8336 lsp::ServerCapabilities {
8337 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8338 ..Default::default()
8339 },
8340 cx,
8341 )
8342 .await;
8343
8344 cx.set_state(indoc! {"
8345 line 1
8346 line 2
8347 linˇe 3
8348 line 4
8349 line 5
8350 "});
8351
8352 // Make an edit
8353 cx.update_editor(|editor, window, cx| {
8354 editor.handle_input("X", window, cx);
8355 });
8356
8357 // Move cursor to a different position
8358 cx.update_editor(|editor, window, cx| {
8359 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8360 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8361 });
8362 });
8363
8364 cx.assert_editor_state(indoc! {"
8365 line 1
8366 line 2
8367 linXe 3
8368 line 4
8369 liˇne 5
8370 "});
8371
8372 cx.lsp
8373 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8374 Ok(Some(vec![lsp::TextEdit::new(
8375 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8376 "PREFIX ".to_string(),
8377 )]))
8378 });
8379
8380 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8381 .unwrap()
8382 .await
8383 .unwrap();
8384
8385 cx.assert_editor_state(indoc! {"
8386 PREFIX line 1
8387 line 2
8388 linXe 3
8389 line 4
8390 liˇne 5
8391 "});
8392
8393 // Undo formatting
8394 cx.update_editor(|editor, window, cx| {
8395 editor.undo(&Default::default(), window, cx);
8396 });
8397
8398 // Verify cursor moved back to position after edit
8399 cx.assert_editor_state(indoc! {"
8400 line 1
8401 line 2
8402 linXˇe 3
8403 line 4
8404 line 5
8405 "});
8406}
8407
8408#[gpui::test]
8409async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8410 init_test(cx, |_| {});
8411
8412 let mut cx = EditorTestContext::new(cx).await;
8413
8414 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8415 cx.update_editor(|editor, window, cx| {
8416 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8417 });
8418
8419 cx.set_state(indoc! {"
8420 line 1
8421 line 2
8422 linˇe 3
8423 line 4
8424 line 5
8425 line 6
8426 line 7
8427 line 8
8428 line 9
8429 line 10
8430 "});
8431
8432 let snapshot = cx.buffer_snapshot();
8433 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8434
8435 cx.update(|_, cx| {
8436 provider.update(cx, |provider, _| {
8437 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8438 id: None,
8439 edits: vec![(edit_position..edit_position, "X".into())],
8440 edit_preview: None,
8441 }))
8442 })
8443 });
8444
8445 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8446 cx.update_editor(|editor, window, cx| {
8447 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8448 });
8449
8450 cx.assert_editor_state(indoc! {"
8451 line 1
8452 line 2
8453 lineXˇ 3
8454 line 4
8455 line 5
8456 line 6
8457 line 7
8458 line 8
8459 line 9
8460 line 10
8461 "});
8462
8463 cx.update_editor(|editor, window, cx| {
8464 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8465 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8466 });
8467 });
8468
8469 cx.assert_editor_state(indoc! {"
8470 line 1
8471 line 2
8472 lineX 3
8473 line 4
8474 line 5
8475 line 6
8476 line 7
8477 line 8
8478 line 9
8479 liˇne 10
8480 "});
8481
8482 cx.update_editor(|editor, window, cx| {
8483 editor.undo(&Default::default(), window, cx);
8484 });
8485
8486 cx.assert_editor_state(indoc! {"
8487 line 1
8488 line 2
8489 lineˇ 3
8490 line 4
8491 line 5
8492 line 6
8493 line 7
8494 line 8
8495 line 9
8496 line 10
8497 "});
8498}
8499
8500#[gpui::test]
8501async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8502 init_test(cx, |_| {});
8503
8504 let mut cx = EditorTestContext::new(cx).await;
8505 cx.set_state(
8506 r#"let foo = 2;
8507lˇet foo = 2;
8508let fooˇ = 2;
8509let foo = 2;
8510let foo = ˇ2;"#,
8511 );
8512
8513 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8514 .unwrap();
8515 cx.assert_editor_state(
8516 r#"let foo = 2;
8517«letˇ» foo = 2;
8518let «fooˇ» = 2;
8519let foo = 2;
8520let foo = «2ˇ»;"#,
8521 );
8522
8523 // noop for multiple selections with different contents
8524 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8525 .unwrap();
8526 cx.assert_editor_state(
8527 r#"let foo = 2;
8528«letˇ» foo = 2;
8529let «fooˇ» = 2;
8530let foo = 2;
8531let foo = «2ˇ»;"#,
8532 );
8533
8534 // Test last selection direction should be preserved
8535 cx.set_state(
8536 r#"let foo = 2;
8537let foo = 2;
8538let «fooˇ» = 2;
8539let «ˇfoo» = 2;
8540let foo = 2;"#,
8541 );
8542
8543 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8544 .unwrap();
8545 cx.assert_editor_state(
8546 r#"let foo = 2;
8547let foo = 2;
8548let «fooˇ» = 2;
8549let «ˇfoo» = 2;
8550let «ˇfoo» = 2;"#,
8551 );
8552}
8553
8554#[gpui::test]
8555async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8556 init_test(cx, |_| {});
8557
8558 let mut cx =
8559 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8560
8561 cx.assert_editor_state(indoc! {"
8562 ˇbbb
8563 ccc
8564
8565 bbb
8566 ccc
8567 "});
8568 cx.dispatch_action(SelectPrevious::default());
8569 cx.assert_editor_state(indoc! {"
8570 «bbbˇ»
8571 ccc
8572
8573 bbb
8574 ccc
8575 "});
8576 cx.dispatch_action(SelectPrevious::default());
8577 cx.assert_editor_state(indoc! {"
8578 «bbbˇ»
8579 ccc
8580
8581 «bbbˇ»
8582 ccc
8583 "});
8584}
8585
8586#[gpui::test]
8587async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8588 init_test(cx, |_| {});
8589
8590 let mut cx = EditorTestContext::new(cx).await;
8591 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8592
8593 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8594 .unwrap();
8595 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8596
8597 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8598 .unwrap();
8599 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8600
8601 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8602 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8603
8604 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8605 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8606
8607 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8608 .unwrap();
8609 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8610
8611 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8612 .unwrap();
8613 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8614}
8615
8616#[gpui::test]
8617async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8618 init_test(cx, |_| {});
8619
8620 let mut cx = EditorTestContext::new(cx).await;
8621 cx.set_state("aˇ");
8622
8623 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8624 .unwrap();
8625 cx.assert_editor_state("«aˇ»");
8626 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8627 .unwrap();
8628 cx.assert_editor_state("«aˇ»");
8629}
8630
8631#[gpui::test]
8632async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8633 init_test(cx, |_| {});
8634
8635 let mut cx = EditorTestContext::new(cx).await;
8636 cx.set_state(
8637 r#"let foo = 2;
8638lˇet foo = 2;
8639let fooˇ = 2;
8640let foo = 2;
8641let foo = ˇ2;"#,
8642 );
8643
8644 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8645 .unwrap();
8646 cx.assert_editor_state(
8647 r#"let foo = 2;
8648«letˇ» foo = 2;
8649let «fooˇ» = 2;
8650let foo = 2;
8651let foo = «2ˇ»;"#,
8652 );
8653
8654 // noop for multiple selections with different contents
8655 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8656 .unwrap();
8657 cx.assert_editor_state(
8658 r#"let foo = 2;
8659«letˇ» foo = 2;
8660let «fooˇ» = 2;
8661let foo = 2;
8662let foo = «2ˇ»;"#,
8663 );
8664}
8665
8666#[gpui::test]
8667async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8668 init_test(cx, |_| {});
8669
8670 let mut cx = EditorTestContext::new(cx).await;
8671 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8672
8673 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8674 .unwrap();
8675 // selection direction is preserved
8676 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8677
8678 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8679 .unwrap();
8680 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8681
8682 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8683 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8684
8685 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8686 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8687
8688 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8689 .unwrap();
8690 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8691
8692 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8693 .unwrap();
8694 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8695}
8696
8697#[gpui::test]
8698async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8699 init_test(cx, |_| {});
8700
8701 let language = Arc::new(Language::new(
8702 LanguageConfig::default(),
8703 Some(tree_sitter_rust::LANGUAGE.into()),
8704 ));
8705
8706 let text = r#"
8707 use mod1::mod2::{mod3, mod4};
8708
8709 fn fn_1(param1: bool, param2: &str) {
8710 let var1 = "text";
8711 }
8712 "#
8713 .unindent();
8714
8715 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8716 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8717 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8718
8719 editor
8720 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8721 .await;
8722
8723 editor.update_in(cx, |editor, window, cx| {
8724 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8725 s.select_display_ranges([
8726 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8727 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8728 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8729 ]);
8730 });
8731 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8732 });
8733 editor.update(cx, |editor, cx| {
8734 assert_text_with_selections(
8735 editor,
8736 indoc! {r#"
8737 use mod1::mod2::{mod3, «mod4ˇ»};
8738
8739 fn fn_1«ˇ(param1: bool, param2: &str)» {
8740 let var1 = "«ˇtext»";
8741 }
8742 "#},
8743 cx,
8744 );
8745 });
8746
8747 editor.update_in(cx, |editor, window, cx| {
8748 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8749 });
8750 editor.update(cx, |editor, cx| {
8751 assert_text_with_selections(
8752 editor,
8753 indoc! {r#"
8754 use mod1::mod2::«{mod3, mod4}ˇ»;
8755
8756 «ˇfn fn_1(param1: bool, param2: &str) {
8757 let var1 = "text";
8758 }»
8759 "#},
8760 cx,
8761 );
8762 });
8763
8764 editor.update_in(cx, |editor, window, cx| {
8765 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8766 });
8767 assert_eq!(
8768 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8769 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8770 );
8771
8772 // Trying to expand the selected syntax node one more time has no effect.
8773 editor.update_in(cx, |editor, window, cx| {
8774 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8775 });
8776 assert_eq!(
8777 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8778 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8779 );
8780
8781 editor.update_in(cx, |editor, window, cx| {
8782 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8783 });
8784 editor.update(cx, |editor, cx| {
8785 assert_text_with_selections(
8786 editor,
8787 indoc! {r#"
8788 use mod1::mod2::«{mod3, mod4}ˇ»;
8789
8790 «ˇfn fn_1(param1: bool, param2: &str) {
8791 let var1 = "text";
8792 }»
8793 "#},
8794 cx,
8795 );
8796 });
8797
8798 editor.update_in(cx, |editor, window, cx| {
8799 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8800 });
8801 editor.update(cx, |editor, cx| {
8802 assert_text_with_selections(
8803 editor,
8804 indoc! {r#"
8805 use mod1::mod2::{mod3, «mod4ˇ»};
8806
8807 fn fn_1«ˇ(param1: bool, param2: &str)» {
8808 let var1 = "«ˇtext»";
8809 }
8810 "#},
8811 cx,
8812 );
8813 });
8814
8815 editor.update_in(cx, |editor, window, cx| {
8816 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8817 });
8818 editor.update(cx, |editor, cx| {
8819 assert_text_with_selections(
8820 editor,
8821 indoc! {r#"
8822 use mod1::mod2::{mod3, moˇd4};
8823
8824 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8825 let var1 = "teˇxt";
8826 }
8827 "#},
8828 cx,
8829 );
8830 });
8831
8832 // Trying to shrink the selected syntax node one more time has no effect.
8833 editor.update_in(cx, |editor, window, cx| {
8834 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8835 });
8836 editor.update_in(cx, |editor, _, cx| {
8837 assert_text_with_selections(
8838 editor,
8839 indoc! {r#"
8840 use mod1::mod2::{mod3, moˇd4};
8841
8842 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8843 let var1 = "teˇxt";
8844 }
8845 "#},
8846 cx,
8847 );
8848 });
8849
8850 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8851 // a fold.
8852 editor.update_in(cx, |editor, window, cx| {
8853 editor.fold_creases(
8854 vec![
8855 Crease::simple(
8856 Point::new(0, 21)..Point::new(0, 24),
8857 FoldPlaceholder::test(),
8858 ),
8859 Crease::simple(
8860 Point::new(3, 20)..Point::new(3, 22),
8861 FoldPlaceholder::test(),
8862 ),
8863 ],
8864 true,
8865 window,
8866 cx,
8867 );
8868 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8869 });
8870 editor.update(cx, |editor, cx| {
8871 assert_text_with_selections(
8872 editor,
8873 indoc! {r#"
8874 use mod1::mod2::«{mod3, mod4}ˇ»;
8875
8876 fn fn_1«ˇ(param1: bool, param2: &str)» {
8877 let var1 = "«ˇtext»";
8878 }
8879 "#},
8880 cx,
8881 );
8882 });
8883}
8884
8885#[gpui::test]
8886async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8887 init_test(cx, |_| {});
8888
8889 let language = Arc::new(Language::new(
8890 LanguageConfig::default(),
8891 Some(tree_sitter_rust::LANGUAGE.into()),
8892 ));
8893
8894 let text = "let a = 2;";
8895
8896 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8897 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8898 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8899
8900 editor
8901 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8902 .await;
8903
8904 // Test case 1: Cursor at end of word
8905 editor.update_in(cx, |editor, window, cx| {
8906 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8907 s.select_display_ranges([
8908 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
8909 ]);
8910 });
8911 });
8912 editor.update(cx, |editor, cx| {
8913 assert_text_with_selections(editor, "let aˇ = 2;", cx);
8914 });
8915 editor.update_in(cx, |editor, window, cx| {
8916 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8917 });
8918 editor.update(cx, |editor, cx| {
8919 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
8920 });
8921 editor.update_in(cx, |editor, window, cx| {
8922 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8923 });
8924 editor.update(cx, |editor, cx| {
8925 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8926 });
8927
8928 // Test case 2: Cursor at end of statement
8929 editor.update_in(cx, |editor, window, cx| {
8930 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8931 s.select_display_ranges([
8932 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
8933 ]);
8934 });
8935 });
8936 editor.update(cx, |editor, cx| {
8937 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
8938 });
8939 editor.update_in(cx, |editor, window, cx| {
8940 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8941 });
8942 editor.update(cx, |editor, cx| {
8943 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8944 });
8945}
8946
8947#[gpui::test]
8948async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
8949 init_test(cx, |_| {});
8950
8951 let language = Arc::new(Language::new(
8952 LanguageConfig {
8953 name: "JavaScript".into(),
8954 ..Default::default()
8955 },
8956 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8957 ));
8958
8959 let text = r#"
8960 let a = {
8961 key: "value",
8962 };
8963 "#
8964 .unindent();
8965
8966 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8967 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8968 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8969
8970 editor
8971 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8972 .await;
8973
8974 // Test case 1: Cursor after '{'
8975 editor.update_in(cx, |editor, window, cx| {
8976 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8977 s.select_display_ranges([
8978 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
8979 ]);
8980 });
8981 });
8982 editor.update(cx, |editor, cx| {
8983 assert_text_with_selections(
8984 editor,
8985 indoc! {r#"
8986 let a = {ˇ
8987 key: "value",
8988 };
8989 "#},
8990 cx,
8991 );
8992 });
8993 editor.update_in(cx, |editor, window, cx| {
8994 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8995 });
8996 editor.update(cx, |editor, cx| {
8997 assert_text_with_selections(
8998 editor,
8999 indoc! {r#"
9000 let a = «ˇ{
9001 key: "value",
9002 }»;
9003 "#},
9004 cx,
9005 );
9006 });
9007
9008 // Test case 2: Cursor after ':'
9009 editor.update_in(cx, |editor, window, cx| {
9010 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9011 s.select_display_ranges([
9012 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9013 ]);
9014 });
9015 });
9016 editor.update(cx, |editor, cx| {
9017 assert_text_with_selections(
9018 editor,
9019 indoc! {r#"
9020 let a = {
9021 key:ˇ "value",
9022 };
9023 "#},
9024 cx,
9025 );
9026 });
9027 editor.update_in(cx, |editor, window, cx| {
9028 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9029 });
9030 editor.update(cx, |editor, cx| {
9031 assert_text_with_selections(
9032 editor,
9033 indoc! {r#"
9034 let a = {
9035 «ˇkey: "value"»,
9036 };
9037 "#},
9038 cx,
9039 );
9040 });
9041 editor.update_in(cx, |editor, window, cx| {
9042 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9043 });
9044 editor.update(cx, |editor, cx| {
9045 assert_text_with_selections(
9046 editor,
9047 indoc! {r#"
9048 let a = «ˇ{
9049 key: "value",
9050 }»;
9051 "#},
9052 cx,
9053 );
9054 });
9055
9056 // Test case 3: Cursor after ','
9057 editor.update_in(cx, |editor, window, cx| {
9058 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9059 s.select_display_ranges([
9060 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9061 ]);
9062 });
9063 });
9064 editor.update(cx, |editor, cx| {
9065 assert_text_with_selections(
9066 editor,
9067 indoc! {r#"
9068 let a = {
9069 key: "value",ˇ
9070 };
9071 "#},
9072 cx,
9073 );
9074 });
9075 editor.update_in(cx, |editor, window, cx| {
9076 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9077 });
9078 editor.update(cx, |editor, cx| {
9079 assert_text_with_selections(
9080 editor,
9081 indoc! {r#"
9082 let a = «ˇ{
9083 key: "value",
9084 }»;
9085 "#},
9086 cx,
9087 );
9088 });
9089
9090 // Test case 4: Cursor after ';'
9091 editor.update_in(cx, |editor, window, cx| {
9092 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9093 s.select_display_ranges([
9094 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9095 ]);
9096 });
9097 });
9098 editor.update(cx, |editor, cx| {
9099 assert_text_with_selections(
9100 editor,
9101 indoc! {r#"
9102 let a = {
9103 key: "value",
9104 };ˇ
9105 "#},
9106 cx,
9107 );
9108 });
9109 editor.update_in(cx, |editor, window, cx| {
9110 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9111 });
9112 editor.update(cx, |editor, cx| {
9113 assert_text_with_selections(
9114 editor,
9115 indoc! {r#"
9116 «ˇlet a = {
9117 key: "value",
9118 };
9119 »"#},
9120 cx,
9121 );
9122 });
9123}
9124
9125#[gpui::test]
9126async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9127 init_test(cx, |_| {});
9128
9129 let language = Arc::new(Language::new(
9130 LanguageConfig::default(),
9131 Some(tree_sitter_rust::LANGUAGE.into()),
9132 ));
9133
9134 let text = r#"
9135 use mod1::mod2::{mod3, mod4};
9136
9137 fn fn_1(param1: bool, param2: &str) {
9138 let var1 = "hello world";
9139 }
9140 "#
9141 .unindent();
9142
9143 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9144 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9145 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9146
9147 editor
9148 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9149 .await;
9150
9151 // Test 1: Cursor on a letter of a string word
9152 editor.update_in(cx, |editor, window, cx| {
9153 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9154 s.select_display_ranges([
9155 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9156 ]);
9157 });
9158 });
9159 editor.update_in(cx, |editor, window, cx| {
9160 assert_text_with_selections(
9161 editor,
9162 indoc! {r#"
9163 use mod1::mod2::{mod3, mod4};
9164
9165 fn fn_1(param1: bool, param2: &str) {
9166 let var1 = "hˇello world";
9167 }
9168 "#},
9169 cx,
9170 );
9171 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9172 assert_text_with_selections(
9173 editor,
9174 indoc! {r#"
9175 use mod1::mod2::{mod3, mod4};
9176
9177 fn fn_1(param1: bool, param2: &str) {
9178 let var1 = "«ˇhello» world";
9179 }
9180 "#},
9181 cx,
9182 );
9183 });
9184
9185 // Test 2: Partial selection within a word
9186 editor.update_in(cx, |editor, window, cx| {
9187 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9188 s.select_display_ranges([
9189 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9190 ]);
9191 });
9192 });
9193 editor.update_in(cx, |editor, window, cx| {
9194 assert_text_with_selections(
9195 editor,
9196 indoc! {r#"
9197 use mod1::mod2::{mod3, mod4};
9198
9199 fn fn_1(param1: bool, param2: &str) {
9200 let var1 = "h«elˇ»lo world";
9201 }
9202 "#},
9203 cx,
9204 );
9205 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9206 assert_text_with_selections(
9207 editor,
9208 indoc! {r#"
9209 use mod1::mod2::{mod3, mod4};
9210
9211 fn fn_1(param1: bool, param2: &str) {
9212 let var1 = "«ˇhello» world";
9213 }
9214 "#},
9215 cx,
9216 );
9217 });
9218
9219 // Test 3: Complete word already selected
9220 editor.update_in(cx, |editor, window, cx| {
9221 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9222 s.select_display_ranges([
9223 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9224 ]);
9225 });
9226 });
9227 editor.update_in(cx, |editor, window, cx| {
9228 assert_text_with_selections(
9229 editor,
9230 indoc! {r#"
9231 use mod1::mod2::{mod3, mod4};
9232
9233 fn fn_1(param1: bool, param2: &str) {
9234 let var1 = "«helloˇ» world";
9235 }
9236 "#},
9237 cx,
9238 );
9239 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9240 assert_text_with_selections(
9241 editor,
9242 indoc! {r#"
9243 use mod1::mod2::{mod3, mod4};
9244
9245 fn fn_1(param1: bool, param2: &str) {
9246 let var1 = "«hello worldˇ»";
9247 }
9248 "#},
9249 cx,
9250 );
9251 });
9252
9253 // Test 4: Selection spanning across words
9254 editor.update_in(cx, |editor, window, cx| {
9255 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9256 s.select_display_ranges([
9257 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9258 ]);
9259 });
9260 });
9261 editor.update_in(cx, |editor, window, cx| {
9262 assert_text_with_selections(
9263 editor,
9264 indoc! {r#"
9265 use mod1::mod2::{mod3, mod4};
9266
9267 fn fn_1(param1: bool, param2: &str) {
9268 let var1 = "hel«lo woˇ»rld";
9269 }
9270 "#},
9271 cx,
9272 );
9273 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9274 assert_text_with_selections(
9275 editor,
9276 indoc! {r#"
9277 use mod1::mod2::{mod3, mod4};
9278
9279 fn fn_1(param1: bool, param2: &str) {
9280 let var1 = "«ˇhello world»";
9281 }
9282 "#},
9283 cx,
9284 );
9285 });
9286
9287 // Test 5: Expansion beyond string
9288 editor.update_in(cx, |editor, window, cx| {
9289 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9290 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9291 assert_text_with_selections(
9292 editor,
9293 indoc! {r#"
9294 use mod1::mod2::{mod3, mod4};
9295
9296 fn fn_1(param1: bool, param2: &str) {
9297 «ˇlet var1 = "hello world";»
9298 }
9299 "#},
9300 cx,
9301 );
9302 });
9303}
9304
9305#[gpui::test]
9306async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9307 init_test(cx, |_| {});
9308
9309 let mut cx = EditorTestContext::new(cx).await;
9310
9311 let language = Arc::new(Language::new(
9312 LanguageConfig::default(),
9313 Some(tree_sitter_rust::LANGUAGE.into()),
9314 ));
9315
9316 cx.update_buffer(|buffer, cx| {
9317 buffer.set_language(Some(language), cx);
9318 });
9319
9320 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9321 cx.update_editor(|editor, window, cx| {
9322 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9323 });
9324
9325 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9326
9327 cx.set_state(indoc! { r#"fn a() {
9328 // what
9329 // a
9330 // ˇlong
9331 // method
9332 // I
9333 // sure
9334 // hope
9335 // it
9336 // works
9337 }"# });
9338
9339 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9340 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9341 cx.update(|_, cx| {
9342 multi_buffer.update(cx, |multi_buffer, cx| {
9343 multi_buffer.set_excerpts_for_path(
9344 PathKey::for_buffer(&buffer, cx),
9345 buffer,
9346 [Point::new(1, 0)..Point::new(1, 0)],
9347 3,
9348 cx,
9349 );
9350 });
9351 });
9352
9353 let editor2 = cx.new_window_entity(|window, cx| {
9354 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9355 });
9356
9357 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9358 cx.update_editor(|editor, window, cx| {
9359 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9360 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9361 })
9362 });
9363
9364 cx.assert_editor_state(indoc! { "
9365 fn a() {
9366 // what
9367 // a
9368 ˇ // long
9369 // method"});
9370
9371 cx.update_editor(|editor, window, cx| {
9372 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9373 });
9374
9375 // Although we could potentially make the action work when the syntax node
9376 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9377 // did. Maybe we could also expand the excerpt to contain the range?
9378 cx.assert_editor_state(indoc! { "
9379 fn a() {
9380 // what
9381 // a
9382 ˇ // long
9383 // method"});
9384}
9385
9386#[gpui::test]
9387async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9388 init_test(cx, |_| {});
9389
9390 let base_text = r#"
9391 impl A {
9392 // this is an uncommitted comment
9393
9394 fn b() {
9395 c();
9396 }
9397
9398 // this is another uncommitted comment
9399
9400 fn d() {
9401 // e
9402 // f
9403 }
9404 }
9405
9406 fn g() {
9407 // h
9408 }
9409 "#
9410 .unindent();
9411
9412 let text = r#"
9413 ˇimpl A {
9414
9415 fn b() {
9416 c();
9417 }
9418
9419 fn d() {
9420 // e
9421 // f
9422 }
9423 }
9424
9425 fn g() {
9426 // h
9427 }
9428 "#
9429 .unindent();
9430
9431 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9432 cx.set_state(&text);
9433 cx.set_head_text(&base_text);
9434 cx.update_editor(|editor, window, cx| {
9435 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9436 });
9437
9438 cx.assert_state_with_diff(
9439 "
9440 ˇimpl A {
9441 - // this is an uncommitted comment
9442
9443 fn b() {
9444 c();
9445 }
9446
9447 - // this is another uncommitted comment
9448 -
9449 fn d() {
9450 // e
9451 // f
9452 }
9453 }
9454
9455 fn g() {
9456 // h
9457 }
9458 "
9459 .unindent(),
9460 );
9461
9462 let expected_display_text = "
9463 impl A {
9464 // this is an uncommitted comment
9465
9466 fn b() {
9467 ⋯
9468 }
9469
9470 // this is another uncommitted comment
9471
9472 fn d() {
9473 ⋯
9474 }
9475 }
9476
9477 fn g() {
9478 ⋯
9479 }
9480 "
9481 .unindent();
9482
9483 cx.update_editor(|editor, window, cx| {
9484 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9485 assert_eq!(editor.display_text(cx), expected_display_text);
9486 });
9487}
9488
9489#[gpui::test]
9490async fn test_autoindent(cx: &mut TestAppContext) {
9491 init_test(cx, |_| {});
9492
9493 let language = Arc::new(
9494 Language::new(
9495 LanguageConfig {
9496 brackets: BracketPairConfig {
9497 pairs: vec![
9498 BracketPair {
9499 start: "{".to_string(),
9500 end: "}".to_string(),
9501 close: false,
9502 surround: false,
9503 newline: true,
9504 },
9505 BracketPair {
9506 start: "(".to_string(),
9507 end: ")".to_string(),
9508 close: false,
9509 surround: false,
9510 newline: true,
9511 },
9512 ],
9513 ..Default::default()
9514 },
9515 ..Default::default()
9516 },
9517 Some(tree_sitter_rust::LANGUAGE.into()),
9518 )
9519 .with_indents_query(
9520 r#"
9521 (_ "(" ")" @end) @indent
9522 (_ "{" "}" @end) @indent
9523 "#,
9524 )
9525 .unwrap(),
9526 );
9527
9528 let text = "fn a() {}";
9529
9530 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9531 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9532 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9533 editor
9534 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9535 .await;
9536
9537 editor.update_in(cx, |editor, window, cx| {
9538 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9539 s.select_ranges([5..5, 8..8, 9..9])
9540 });
9541 editor.newline(&Newline, window, cx);
9542 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9543 assert_eq!(
9544 editor.selections.ranges(cx),
9545 &[
9546 Point::new(1, 4)..Point::new(1, 4),
9547 Point::new(3, 4)..Point::new(3, 4),
9548 Point::new(5, 0)..Point::new(5, 0)
9549 ]
9550 );
9551 });
9552}
9553
9554#[gpui::test]
9555async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9556 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9557
9558 let language = Arc::new(
9559 Language::new(
9560 LanguageConfig {
9561 brackets: BracketPairConfig {
9562 pairs: vec![
9563 BracketPair {
9564 start: "{".to_string(),
9565 end: "}".to_string(),
9566 close: false,
9567 surround: false,
9568 newline: true,
9569 },
9570 BracketPair {
9571 start: "(".to_string(),
9572 end: ")".to_string(),
9573 close: false,
9574 surround: false,
9575 newline: true,
9576 },
9577 ],
9578 ..Default::default()
9579 },
9580 ..Default::default()
9581 },
9582 Some(tree_sitter_rust::LANGUAGE.into()),
9583 )
9584 .with_indents_query(
9585 r#"
9586 (_ "(" ")" @end) @indent
9587 (_ "{" "}" @end) @indent
9588 "#,
9589 )
9590 .unwrap(),
9591 );
9592
9593 let text = "fn a() {}";
9594
9595 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9596 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9597 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9598 editor
9599 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9600 .await;
9601
9602 editor.update_in(cx, |editor, window, cx| {
9603 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9604 s.select_ranges([5..5, 8..8, 9..9])
9605 });
9606 editor.newline(&Newline, window, cx);
9607 assert_eq!(
9608 editor.text(cx),
9609 indoc!(
9610 "
9611 fn a(
9612
9613 ) {
9614
9615 }
9616 "
9617 )
9618 );
9619 assert_eq!(
9620 editor.selections.ranges(cx),
9621 &[
9622 Point::new(1, 0)..Point::new(1, 0),
9623 Point::new(3, 0)..Point::new(3, 0),
9624 Point::new(5, 0)..Point::new(5, 0)
9625 ]
9626 );
9627 });
9628}
9629
9630#[gpui::test]
9631async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9632 init_test(cx, |settings| {
9633 settings.defaults.auto_indent = Some(true);
9634 settings.languages.0.insert(
9635 "python".into(),
9636 LanguageSettingsContent {
9637 auto_indent: Some(false),
9638 ..Default::default()
9639 },
9640 );
9641 });
9642
9643 let mut cx = EditorTestContext::new(cx).await;
9644
9645 let injected_language = Arc::new(
9646 Language::new(
9647 LanguageConfig {
9648 brackets: BracketPairConfig {
9649 pairs: vec![
9650 BracketPair {
9651 start: "{".to_string(),
9652 end: "}".to_string(),
9653 close: false,
9654 surround: false,
9655 newline: true,
9656 },
9657 BracketPair {
9658 start: "(".to_string(),
9659 end: ")".to_string(),
9660 close: true,
9661 surround: false,
9662 newline: true,
9663 },
9664 ],
9665 ..Default::default()
9666 },
9667 name: "python".into(),
9668 ..Default::default()
9669 },
9670 Some(tree_sitter_python::LANGUAGE.into()),
9671 )
9672 .with_indents_query(
9673 r#"
9674 (_ "(" ")" @end) @indent
9675 (_ "{" "}" @end) @indent
9676 "#,
9677 )
9678 .unwrap(),
9679 );
9680
9681 let language = Arc::new(
9682 Language::new(
9683 LanguageConfig {
9684 brackets: BracketPairConfig {
9685 pairs: vec![
9686 BracketPair {
9687 start: "{".to_string(),
9688 end: "}".to_string(),
9689 close: false,
9690 surround: false,
9691 newline: true,
9692 },
9693 BracketPair {
9694 start: "(".to_string(),
9695 end: ")".to_string(),
9696 close: true,
9697 surround: false,
9698 newline: true,
9699 },
9700 ],
9701 ..Default::default()
9702 },
9703 name: LanguageName::new("rust"),
9704 ..Default::default()
9705 },
9706 Some(tree_sitter_rust::LANGUAGE.into()),
9707 )
9708 .with_indents_query(
9709 r#"
9710 (_ "(" ")" @end) @indent
9711 (_ "{" "}" @end) @indent
9712 "#,
9713 )
9714 .unwrap()
9715 .with_injection_query(
9716 r#"
9717 (macro_invocation
9718 macro: (identifier) @_macro_name
9719 (token_tree) @injection.content
9720 (#set! injection.language "python"))
9721 "#,
9722 )
9723 .unwrap(),
9724 );
9725
9726 cx.language_registry().add(injected_language);
9727 cx.language_registry().add(language.clone());
9728
9729 cx.update_buffer(|buffer, cx| {
9730 buffer.set_language(Some(language), cx);
9731 });
9732
9733 cx.set_state(r#"struct A {ˇ}"#);
9734
9735 cx.update_editor(|editor, window, cx| {
9736 editor.newline(&Default::default(), window, cx);
9737 });
9738
9739 cx.assert_editor_state(indoc!(
9740 "struct A {
9741 ˇ
9742 }"
9743 ));
9744
9745 cx.set_state(r#"select_biased!(ˇ)"#);
9746
9747 cx.update_editor(|editor, window, cx| {
9748 editor.newline(&Default::default(), window, cx);
9749 editor.handle_input("def ", window, cx);
9750 editor.handle_input("(", window, cx);
9751 editor.newline(&Default::default(), window, cx);
9752 editor.handle_input("a", window, cx);
9753 });
9754
9755 cx.assert_editor_state(indoc!(
9756 "select_biased!(
9757 def (
9758 aˇ
9759 )
9760 )"
9761 ));
9762}
9763
9764#[gpui::test]
9765async fn test_autoindent_selections(cx: &mut TestAppContext) {
9766 init_test(cx, |_| {});
9767
9768 {
9769 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9770 cx.set_state(indoc! {"
9771 impl A {
9772
9773 fn b() {}
9774
9775 «fn c() {
9776
9777 }ˇ»
9778 }
9779 "});
9780
9781 cx.update_editor(|editor, window, cx| {
9782 editor.autoindent(&Default::default(), window, cx);
9783 });
9784
9785 cx.assert_editor_state(indoc! {"
9786 impl A {
9787
9788 fn b() {}
9789
9790 «fn c() {
9791
9792 }ˇ»
9793 }
9794 "});
9795 }
9796
9797 {
9798 let mut cx = EditorTestContext::new_multibuffer(
9799 cx,
9800 [indoc! { "
9801 impl A {
9802 «
9803 // a
9804 fn b(){}
9805 »
9806 «
9807 }
9808 fn c(){}
9809 »
9810 "}],
9811 );
9812
9813 let buffer = cx.update_editor(|editor, _, cx| {
9814 let buffer = editor.buffer().update(cx, |buffer, _| {
9815 buffer.all_buffers().iter().next().unwrap().clone()
9816 });
9817 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9818 buffer
9819 });
9820
9821 cx.run_until_parked();
9822 cx.update_editor(|editor, window, cx| {
9823 editor.select_all(&Default::default(), window, cx);
9824 editor.autoindent(&Default::default(), window, cx)
9825 });
9826 cx.run_until_parked();
9827
9828 cx.update(|_, cx| {
9829 assert_eq!(
9830 buffer.read(cx).text(),
9831 indoc! { "
9832 impl A {
9833
9834 // a
9835 fn b(){}
9836
9837
9838 }
9839 fn c(){}
9840
9841 " }
9842 )
9843 });
9844 }
9845}
9846
9847#[gpui::test]
9848async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9849 init_test(cx, |_| {});
9850
9851 let mut cx = EditorTestContext::new(cx).await;
9852
9853 let language = Arc::new(Language::new(
9854 LanguageConfig {
9855 brackets: BracketPairConfig {
9856 pairs: vec![
9857 BracketPair {
9858 start: "{".to_string(),
9859 end: "}".to_string(),
9860 close: true,
9861 surround: true,
9862 newline: true,
9863 },
9864 BracketPair {
9865 start: "(".to_string(),
9866 end: ")".to_string(),
9867 close: true,
9868 surround: true,
9869 newline: true,
9870 },
9871 BracketPair {
9872 start: "/*".to_string(),
9873 end: " */".to_string(),
9874 close: true,
9875 surround: true,
9876 newline: true,
9877 },
9878 BracketPair {
9879 start: "[".to_string(),
9880 end: "]".to_string(),
9881 close: false,
9882 surround: false,
9883 newline: true,
9884 },
9885 BracketPair {
9886 start: "\"".to_string(),
9887 end: "\"".to_string(),
9888 close: true,
9889 surround: true,
9890 newline: false,
9891 },
9892 BracketPair {
9893 start: "<".to_string(),
9894 end: ">".to_string(),
9895 close: false,
9896 surround: true,
9897 newline: true,
9898 },
9899 ],
9900 ..Default::default()
9901 },
9902 autoclose_before: "})]".to_string(),
9903 ..Default::default()
9904 },
9905 Some(tree_sitter_rust::LANGUAGE.into()),
9906 ));
9907
9908 cx.language_registry().add(language.clone());
9909 cx.update_buffer(|buffer, cx| {
9910 buffer.set_language(Some(language), cx);
9911 });
9912
9913 cx.set_state(
9914 &r#"
9915 🏀ˇ
9916 εˇ
9917 ❤️ˇ
9918 "#
9919 .unindent(),
9920 );
9921
9922 // autoclose multiple nested brackets at multiple cursors
9923 cx.update_editor(|editor, window, cx| {
9924 editor.handle_input("{", window, cx);
9925 editor.handle_input("{", window, cx);
9926 editor.handle_input("{", window, cx);
9927 });
9928 cx.assert_editor_state(
9929 &"
9930 🏀{{{ˇ}}}
9931 ε{{{ˇ}}}
9932 ❤️{{{ˇ}}}
9933 "
9934 .unindent(),
9935 );
9936
9937 // insert a different closing bracket
9938 cx.update_editor(|editor, window, cx| {
9939 editor.handle_input(")", window, cx);
9940 });
9941 cx.assert_editor_state(
9942 &"
9943 🏀{{{)ˇ}}}
9944 ε{{{)ˇ}}}
9945 ❤️{{{)ˇ}}}
9946 "
9947 .unindent(),
9948 );
9949
9950 // skip over the auto-closed brackets when typing a closing bracket
9951 cx.update_editor(|editor, window, cx| {
9952 editor.move_right(&MoveRight, window, cx);
9953 editor.handle_input("}", window, cx);
9954 editor.handle_input("}", window, cx);
9955 editor.handle_input("}", window, cx);
9956 });
9957 cx.assert_editor_state(
9958 &"
9959 🏀{{{)}}}}ˇ
9960 ε{{{)}}}}ˇ
9961 ❤️{{{)}}}}ˇ
9962 "
9963 .unindent(),
9964 );
9965
9966 // autoclose multi-character pairs
9967 cx.set_state(
9968 &"
9969 ˇ
9970 ˇ
9971 "
9972 .unindent(),
9973 );
9974 cx.update_editor(|editor, window, cx| {
9975 editor.handle_input("/", window, cx);
9976 editor.handle_input("*", window, cx);
9977 });
9978 cx.assert_editor_state(
9979 &"
9980 /*ˇ */
9981 /*ˇ */
9982 "
9983 .unindent(),
9984 );
9985
9986 // one cursor autocloses a multi-character pair, one cursor
9987 // does not autoclose.
9988 cx.set_state(
9989 &"
9990 /ˇ
9991 ˇ
9992 "
9993 .unindent(),
9994 );
9995 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
9996 cx.assert_editor_state(
9997 &"
9998 /*ˇ */
9999 *ˇ
10000 "
10001 .unindent(),
10002 );
10003
10004 // Don't autoclose if the next character isn't whitespace and isn't
10005 // listed in the language's "autoclose_before" section.
10006 cx.set_state("ˇa b");
10007 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10008 cx.assert_editor_state("{ˇa b");
10009
10010 // Don't autoclose if `close` is false for the bracket pair
10011 cx.set_state("ˇ");
10012 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10013 cx.assert_editor_state("[ˇ");
10014
10015 // Surround with brackets if text is selected
10016 cx.set_state("«aˇ» b");
10017 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10018 cx.assert_editor_state("{«aˇ»} b");
10019
10020 // Autoclose when not immediately after a word character
10021 cx.set_state("a ˇ");
10022 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10023 cx.assert_editor_state("a \"ˇ\"");
10024
10025 // Autoclose pair where the start and end characters are the same
10026 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10027 cx.assert_editor_state("a \"\"ˇ");
10028
10029 // Don't autoclose when immediately after a word character
10030 cx.set_state("aˇ");
10031 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10032 cx.assert_editor_state("a\"ˇ");
10033
10034 // Do autoclose when after a non-word character
10035 cx.set_state("{ˇ");
10036 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10037 cx.assert_editor_state("{\"ˇ\"");
10038
10039 // Non identical pairs autoclose regardless of preceding character
10040 cx.set_state("aˇ");
10041 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10042 cx.assert_editor_state("a{ˇ}");
10043
10044 // Don't autoclose pair if autoclose is disabled
10045 cx.set_state("ˇ");
10046 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10047 cx.assert_editor_state("<ˇ");
10048
10049 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10050 cx.set_state("«aˇ» b");
10051 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10052 cx.assert_editor_state("<«aˇ»> b");
10053}
10054
10055#[gpui::test]
10056async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10057 init_test(cx, |settings| {
10058 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10059 });
10060
10061 let mut cx = EditorTestContext::new(cx).await;
10062
10063 let language = Arc::new(Language::new(
10064 LanguageConfig {
10065 brackets: BracketPairConfig {
10066 pairs: vec![
10067 BracketPair {
10068 start: "{".to_string(),
10069 end: "}".to_string(),
10070 close: true,
10071 surround: true,
10072 newline: true,
10073 },
10074 BracketPair {
10075 start: "(".to_string(),
10076 end: ")".to_string(),
10077 close: true,
10078 surround: true,
10079 newline: true,
10080 },
10081 BracketPair {
10082 start: "[".to_string(),
10083 end: "]".to_string(),
10084 close: false,
10085 surround: false,
10086 newline: true,
10087 },
10088 ],
10089 ..Default::default()
10090 },
10091 autoclose_before: "})]".to_string(),
10092 ..Default::default()
10093 },
10094 Some(tree_sitter_rust::LANGUAGE.into()),
10095 ));
10096
10097 cx.language_registry().add(language.clone());
10098 cx.update_buffer(|buffer, cx| {
10099 buffer.set_language(Some(language), cx);
10100 });
10101
10102 cx.set_state(
10103 &"
10104 ˇ
10105 ˇ
10106 ˇ
10107 "
10108 .unindent(),
10109 );
10110
10111 // ensure only matching closing brackets are skipped over
10112 cx.update_editor(|editor, window, cx| {
10113 editor.handle_input("}", window, cx);
10114 editor.move_left(&MoveLeft, window, cx);
10115 editor.handle_input(")", window, cx);
10116 editor.move_left(&MoveLeft, window, cx);
10117 });
10118 cx.assert_editor_state(
10119 &"
10120 ˇ)}
10121 ˇ)}
10122 ˇ)}
10123 "
10124 .unindent(),
10125 );
10126
10127 // skip-over closing brackets at multiple cursors
10128 cx.update_editor(|editor, window, cx| {
10129 editor.handle_input(")", window, cx);
10130 editor.handle_input("}", window, cx);
10131 });
10132 cx.assert_editor_state(
10133 &"
10134 )}ˇ
10135 )}ˇ
10136 )}ˇ
10137 "
10138 .unindent(),
10139 );
10140
10141 // ignore non-close brackets
10142 cx.update_editor(|editor, window, cx| {
10143 editor.handle_input("]", window, cx);
10144 editor.move_left(&MoveLeft, window, cx);
10145 editor.handle_input("]", window, cx);
10146 });
10147 cx.assert_editor_state(
10148 &"
10149 )}]ˇ]
10150 )}]ˇ]
10151 )}]ˇ]
10152 "
10153 .unindent(),
10154 );
10155}
10156
10157#[gpui::test]
10158async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10159 init_test(cx, |_| {});
10160
10161 let mut cx = EditorTestContext::new(cx).await;
10162
10163 let html_language = Arc::new(
10164 Language::new(
10165 LanguageConfig {
10166 name: "HTML".into(),
10167 brackets: BracketPairConfig {
10168 pairs: vec![
10169 BracketPair {
10170 start: "<".into(),
10171 end: ">".into(),
10172 close: true,
10173 ..Default::default()
10174 },
10175 BracketPair {
10176 start: "{".into(),
10177 end: "}".into(),
10178 close: true,
10179 ..Default::default()
10180 },
10181 BracketPair {
10182 start: "(".into(),
10183 end: ")".into(),
10184 close: true,
10185 ..Default::default()
10186 },
10187 ],
10188 ..Default::default()
10189 },
10190 autoclose_before: "})]>".into(),
10191 ..Default::default()
10192 },
10193 Some(tree_sitter_html::LANGUAGE.into()),
10194 )
10195 .with_injection_query(
10196 r#"
10197 (script_element
10198 (raw_text) @injection.content
10199 (#set! injection.language "javascript"))
10200 "#,
10201 )
10202 .unwrap(),
10203 );
10204
10205 let javascript_language = Arc::new(Language::new(
10206 LanguageConfig {
10207 name: "JavaScript".into(),
10208 brackets: BracketPairConfig {
10209 pairs: vec![
10210 BracketPair {
10211 start: "/*".into(),
10212 end: " */".into(),
10213 close: true,
10214 ..Default::default()
10215 },
10216 BracketPair {
10217 start: "{".into(),
10218 end: "}".into(),
10219 close: true,
10220 ..Default::default()
10221 },
10222 BracketPair {
10223 start: "(".into(),
10224 end: ")".into(),
10225 close: true,
10226 ..Default::default()
10227 },
10228 ],
10229 ..Default::default()
10230 },
10231 autoclose_before: "})]>".into(),
10232 ..Default::default()
10233 },
10234 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10235 ));
10236
10237 cx.language_registry().add(html_language.clone());
10238 cx.language_registry().add(javascript_language);
10239 cx.executor().run_until_parked();
10240
10241 cx.update_buffer(|buffer, cx| {
10242 buffer.set_language(Some(html_language), cx);
10243 });
10244
10245 cx.set_state(
10246 &r#"
10247 <body>ˇ
10248 <script>
10249 var x = 1;ˇ
10250 </script>
10251 </body>ˇ
10252 "#
10253 .unindent(),
10254 );
10255
10256 // Precondition: different languages are active at different locations.
10257 cx.update_editor(|editor, window, cx| {
10258 let snapshot = editor.snapshot(window, cx);
10259 let cursors = editor.selections.ranges::<usize>(cx);
10260 let languages = cursors
10261 .iter()
10262 .map(|c| snapshot.language_at(c.start).unwrap().name())
10263 .collect::<Vec<_>>();
10264 assert_eq!(
10265 languages,
10266 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10267 );
10268 });
10269
10270 // Angle brackets autoclose in HTML, but not JavaScript.
10271 cx.update_editor(|editor, window, cx| {
10272 editor.handle_input("<", window, cx);
10273 editor.handle_input("a", window, cx);
10274 });
10275 cx.assert_editor_state(
10276 &r#"
10277 <body><aˇ>
10278 <script>
10279 var x = 1;<aˇ
10280 </script>
10281 </body><aˇ>
10282 "#
10283 .unindent(),
10284 );
10285
10286 // Curly braces and parens autoclose in both HTML and JavaScript.
10287 cx.update_editor(|editor, window, cx| {
10288 editor.handle_input(" b=", window, cx);
10289 editor.handle_input("{", window, cx);
10290 editor.handle_input("c", window, cx);
10291 editor.handle_input("(", window, cx);
10292 });
10293 cx.assert_editor_state(
10294 &r#"
10295 <body><a b={c(ˇ)}>
10296 <script>
10297 var x = 1;<a b={c(ˇ)}
10298 </script>
10299 </body><a b={c(ˇ)}>
10300 "#
10301 .unindent(),
10302 );
10303
10304 // Brackets that were already autoclosed are skipped.
10305 cx.update_editor(|editor, window, cx| {
10306 editor.handle_input(")", window, cx);
10307 editor.handle_input("d", window, cx);
10308 editor.handle_input("}", window, cx);
10309 });
10310 cx.assert_editor_state(
10311 &r#"
10312 <body><a b={c()d}ˇ>
10313 <script>
10314 var x = 1;<a b={c()d}ˇ
10315 </script>
10316 </body><a b={c()d}ˇ>
10317 "#
10318 .unindent(),
10319 );
10320 cx.update_editor(|editor, window, cx| {
10321 editor.handle_input(">", window, cx);
10322 });
10323 cx.assert_editor_state(
10324 &r#"
10325 <body><a b={c()d}>ˇ
10326 <script>
10327 var x = 1;<a b={c()d}>ˇ
10328 </script>
10329 </body><a b={c()d}>ˇ
10330 "#
10331 .unindent(),
10332 );
10333
10334 // Reset
10335 cx.set_state(
10336 &r#"
10337 <body>ˇ
10338 <script>
10339 var x = 1;ˇ
10340 </script>
10341 </body>ˇ
10342 "#
10343 .unindent(),
10344 );
10345
10346 cx.update_editor(|editor, window, cx| {
10347 editor.handle_input("<", window, cx);
10348 });
10349 cx.assert_editor_state(
10350 &r#"
10351 <body><ˇ>
10352 <script>
10353 var x = 1;<ˇ
10354 </script>
10355 </body><ˇ>
10356 "#
10357 .unindent(),
10358 );
10359
10360 // When backspacing, the closing angle brackets are removed.
10361 cx.update_editor(|editor, window, cx| {
10362 editor.backspace(&Backspace, window, cx);
10363 });
10364 cx.assert_editor_state(
10365 &r#"
10366 <body>ˇ
10367 <script>
10368 var x = 1;ˇ
10369 </script>
10370 </body>ˇ
10371 "#
10372 .unindent(),
10373 );
10374
10375 // Block comments autoclose in JavaScript, but not HTML.
10376 cx.update_editor(|editor, window, cx| {
10377 editor.handle_input("/", window, cx);
10378 editor.handle_input("*", window, cx);
10379 });
10380 cx.assert_editor_state(
10381 &r#"
10382 <body>/*ˇ
10383 <script>
10384 var x = 1;/*ˇ */
10385 </script>
10386 </body>/*ˇ
10387 "#
10388 .unindent(),
10389 );
10390}
10391
10392#[gpui::test]
10393async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10394 init_test(cx, |_| {});
10395
10396 let mut cx = EditorTestContext::new(cx).await;
10397
10398 let rust_language = Arc::new(
10399 Language::new(
10400 LanguageConfig {
10401 name: "Rust".into(),
10402 brackets: serde_json::from_value(json!([
10403 { "start": "{", "end": "}", "close": true, "newline": true },
10404 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10405 ]))
10406 .unwrap(),
10407 autoclose_before: "})]>".into(),
10408 ..Default::default()
10409 },
10410 Some(tree_sitter_rust::LANGUAGE.into()),
10411 )
10412 .with_override_query("(string_literal) @string")
10413 .unwrap(),
10414 );
10415
10416 cx.language_registry().add(rust_language.clone());
10417 cx.update_buffer(|buffer, cx| {
10418 buffer.set_language(Some(rust_language), cx);
10419 });
10420
10421 cx.set_state(
10422 &r#"
10423 let x = ˇ
10424 "#
10425 .unindent(),
10426 );
10427
10428 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10429 cx.update_editor(|editor, window, cx| {
10430 editor.handle_input("\"", window, cx);
10431 });
10432 cx.assert_editor_state(
10433 &r#"
10434 let x = "ˇ"
10435 "#
10436 .unindent(),
10437 );
10438
10439 // Inserting another quotation mark. The cursor moves across the existing
10440 // automatically-inserted quotation mark.
10441 cx.update_editor(|editor, window, cx| {
10442 editor.handle_input("\"", window, cx);
10443 });
10444 cx.assert_editor_state(
10445 &r#"
10446 let x = ""ˇ
10447 "#
10448 .unindent(),
10449 );
10450
10451 // Reset
10452 cx.set_state(
10453 &r#"
10454 let x = ˇ
10455 "#
10456 .unindent(),
10457 );
10458
10459 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10460 cx.update_editor(|editor, window, cx| {
10461 editor.handle_input("\"", window, cx);
10462 editor.handle_input(" ", window, cx);
10463 editor.move_left(&Default::default(), window, cx);
10464 editor.handle_input("\\", window, cx);
10465 editor.handle_input("\"", window, cx);
10466 });
10467 cx.assert_editor_state(
10468 &r#"
10469 let x = "\"ˇ "
10470 "#
10471 .unindent(),
10472 );
10473
10474 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10475 // mark. Nothing is inserted.
10476 cx.update_editor(|editor, window, cx| {
10477 editor.move_right(&Default::default(), window, cx);
10478 editor.handle_input("\"", window, cx);
10479 });
10480 cx.assert_editor_state(
10481 &r#"
10482 let x = "\" "ˇ
10483 "#
10484 .unindent(),
10485 );
10486}
10487
10488#[gpui::test]
10489async fn test_surround_with_pair(cx: &mut TestAppContext) {
10490 init_test(cx, |_| {});
10491
10492 let language = Arc::new(Language::new(
10493 LanguageConfig {
10494 brackets: BracketPairConfig {
10495 pairs: vec![
10496 BracketPair {
10497 start: "{".to_string(),
10498 end: "}".to_string(),
10499 close: true,
10500 surround: true,
10501 newline: true,
10502 },
10503 BracketPair {
10504 start: "/* ".to_string(),
10505 end: "*/".to_string(),
10506 close: true,
10507 surround: true,
10508 ..Default::default()
10509 },
10510 ],
10511 ..Default::default()
10512 },
10513 ..Default::default()
10514 },
10515 Some(tree_sitter_rust::LANGUAGE.into()),
10516 ));
10517
10518 let text = r#"
10519 a
10520 b
10521 c
10522 "#
10523 .unindent();
10524
10525 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10526 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10527 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10528 editor
10529 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10530 .await;
10531
10532 editor.update_in(cx, |editor, window, cx| {
10533 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10534 s.select_display_ranges([
10535 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10536 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10537 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10538 ])
10539 });
10540
10541 editor.handle_input("{", window, cx);
10542 editor.handle_input("{", window, cx);
10543 editor.handle_input("{", window, cx);
10544 assert_eq!(
10545 editor.text(cx),
10546 "
10547 {{{a}}}
10548 {{{b}}}
10549 {{{c}}}
10550 "
10551 .unindent()
10552 );
10553 assert_eq!(
10554 editor.selections.display_ranges(cx),
10555 [
10556 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10557 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10558 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10559 ]
10560 );
10561
10562 editor.undo(&Undo, window, cx);
10563 editor.undo(&Undo, window, cx);
10564 editor.undo(&Undo, window, cx);
10565 assert_eq!(
10566 editor.text(cx),
10567 "
10568 a
10569 b
10570 c
10571 "
10572 .unindent()
10573 );
10574 assert_eq!(
10575 editor.selections.display_ranges(cx),
10576 [
10577 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10578 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10579 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10580 ]
10581 );
10582
10583 // Ensure inserting the first character of a multi-byte bracket pair
10584 // doesn't surround the selections with the bracket.
10585 editor.handle_input("/", window, cx);
10586 assert_eq!(
10587 editor.text(cx),
10588 "
10589 /
10590 /
10591 /
10592 "
10593 .unindent()
10594 );
10595 assert_eq!(
10596 editor.selections.display_ranges(cx),
10597 [
10598 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10599 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10600 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10601 ]
10602 );
10603
10604 editor.undo(&Undo, window, cx);
10605 assert_eq!(
10606 editor.text(cx),
10607 "
10608 a
10609 b
10610 c
10611 "
10612 .unindent()
10613 );
10614 assert_eq!(
10615 editor.selections.display_ranges(cx),
10616 [
10617 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10618 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10619 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10620 ]
10621 );
10622
10623 // Ensure inserting the last character of a multi-byte bracket pair
10624 // doesn't surround the selections with the bracket.
10625 editor.handle_input("*", window, cx);
10626 assert_eq!(
10627 editor.text(cx),
10628 "
10629 *
10630 *
10631 *
10632 "
10633 .unindent()
10634 );
10635 assert_eq!(
10636 editor.selections.display_ranges(cx),
10637 [
10638 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10639 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10640 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10641 ]
10642 );
10643 });
10644}
10645
10646#[gpui::test]
10647async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10648 init_test(cx, |_| {});
10649
10650 let language = Arc::new(Language::new(
10651 LanguageConfig {
10652 brackets: BracketPairConfig {
10653 pairs: vec![BracketPair {
10654 start: "{".to_string(),
10655 end: "}".to_string(),
10656 close: true,
10657 surround: true,
10658 newline: true,
10659 }],
10660 ..Default::default()
10661 },
10662 autoclose_before: "}".to_string(),
10663 ..Default::default()
10664 },
10665 Some(tree_sitter_rust::LANGUAGE.into()),
10666 ));
10667
10668 let text = r#"
10669 a
10670 b
10671 c
10672 "#
10673 .unindent();
10674
10675 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10676 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10677 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10678 editor
10679 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10680 .await;
10681
10682 editor.update_in(cx, |editor, window, cx| {
10683 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10684 s.select_ranges([
10685 Point::new(0, 1)..Point::new(0, 1),
10686 Point::new(1, 1)..Point::new(1, 1),
10687 Point::new(2, 1)..Point::new(2, 1),
10688 ])
10689 });
10690
10691 editor.handle_input("{", window, cx);
10692 editor.handle_input("{", window, cx);
10693 editor.handle_input("_", window, cx);
10694 assert_eq!(
10695 editor.text(cx),
10696 "
10697 a{{_}}
10698 b{{_}}
10699 c{{_}}
10700 "
10701 .unindent()
10702 );
10703 assert_eq!(
10704 editor.selections.ranges::<Point>(cx),
10705 [
10706 Point::new(0, 4)..Point::new(0, 4),
10707 Point::new(1, 4)..Point::new(1, 4),
10708 Point::new(2, 4)..Point::new(2, 4)
10709 ]
10710 );
10711
10712 editor.backspace(&Default::default(), window, cx);
10713 editor.backspace(&Default::default(), window, cx);
10714 assert_eq!(
10715 editor.text(cx),
10716 "
10717 a{}
10718 b{}
10719 c{}
10720 "
10721 .unindent()
10722 );
10723 assert_eq!(
10724 editor.selections.ranges::<Point>(cx),
10725 [
10726 Point::new(0, 2)..Point::new(0, 2),
10727 Point::new(1, 2)..Point::new(1, 2),
10728 Point::new(2, 2)..Point::new(2, 2)
10729 ]
10730 );
10731
10732 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10733 assert_eq!(
10734 editor.text(cx),
10735 "
10736 a
10737 b
10738 c
10739 "
10740 .unindent()
10741 );
10742 assert_eq!(
10743 editor.selections.ranges::<Point>(cx),
10744 [
10745 Point::new(0, 1)..Point::new(0, 1),
10746 Point::new(1, 1)..Point::new(1, 1),
10747 Point::new(2, 1)..Point::new(2, 1)
10748 ]
10749 );
10750 });
10751}
10752
10753#[gpui::test]
10754async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10755 init_test(cx, |settings| {
10756 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10757 });
10758
10759 let mut cx = EditorTestContext::new(cx).await;
10760
10761 let language = Arc::new(Language::new(
10762 LanguageConfig {
10763 brackets: BracketPairConfig {
10764 pairs: vec![
10765 BracketPair {
10766 start: "{".to_string(),
10767 end: "}".to_string(),
10768 close: true,
10769 surround: true,
10770 newline: true,
10771 },
10772 BracketPair {
10773 start: "(".to_string(),
10774 end: ")".to_string(),
10775 close: true,
10776 surround: true,
10777 newline: true,
10778 },
10779 BracketPair {
10780 start: "[".to_string(),
10781 end: "]".to_string(),
10782 close: false,
10783 surround: true,
10784 newline: true,
10785 },
10786 ],
10787 ..Default::default()
10788 },
10789 autoclose_before: "})]".to_string(),
10790 ..Default::default()
10791 },
10792 Some(tree_sitter_rust::LANGUAGE.into()),
10793 ));
10794
10795 cx.language_registry().add(language.clone());
10796 cx.update_buffer(|buffer, cx| {
10797 buffer.set_language(Some(language), cx);
10798 });
10799
10800 cx.set_state(
10801 &"
10802 {(ˇ)}
10803 [[ˇ]]
10804 {(ˇ)}
10805 "
10806 .unindent(),
10807 );
10808
10809 cx.update_editor(|editor, window, cx| {
10810 editor.backspace(&Default::default(), window, cx);
10811 editor.backspace(&Default::default(), window, cx);
10812 });
10813
10814 cx.assert_editor_state(
10815 &"
10816 ˇ
10817 ˇ]]
10818 ˇ
10819 "
10820 .unindent(),
10821 );
10822
10823 cx.update_editor(|editor, window, cx| {
10824 editor.handle_input("{", window, cx);
10825 editor.handle_input("{", window, cx);
10826 editor.move_right(&MoveRight, window, cx);
10827 editor.move_right(&MoveRight, window, cx);
10828 editor.move_left(&MoveLeft, window, cx);
10829 editor.move_left(&MoveLeft, window, cx);
10830 editor.backspace(&Default::default(), window, cx);
10831 });
10832
10833 cx.assert_editor_state(
10834 &"
10835 {ˇ}
10836 {ˇ}]]
10837 {ˇ}
10838 "
10839 .unindent(),
10840 );
10841
10842 cx.update_editor(|editor, window, cx| {
10843 editor.backspace(&Default::default(), window, cx);
10844 });
10845
10846 cx.assert_editor_state(
10847 &"
10848 ˇ
10849 ˇ]]
10850 ˇ
10851 "
10852 .unindent(),
10853 );
10854}
10855
10856#[gpui::test]
10857async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10858 init_test(cx, |_| {});
10859
10860 let language = Arc::new(Language::new(
10861 LanguageConfig::default(),
10862 Some(tree_sitter_rust::LANGUAGE.into()),
10863 ));
10864
10865 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10866 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10867 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10868 editor
10869 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10870 .await;
10871
10872 editor.update_in(cx, |editor, window, cx| {
10873 editor.set_auto_replace_emoji_shortcode(true);
10874
10875 editor.handle_input("Hello ", window, cx);
10876 editor.handle_input(":wave", window, cx);
10877 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10878
10879 editor.handle_input(":", window, cx);
10880 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10881
10882 editor.handle_input(" :smile", window, cx);
10883 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10884
10885 editor.handle_input(":", window, cx);
10886 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10887
10888 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10889 editor.handle_input(":wave", window, cx);
10890 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10891
10892 editor.handle_input(":", window, cx);
10893 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10894
10895 editor.handle_input(":1", window, cx);
10896 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10897
10898 editor.handle_input(":", window, cx);
10899 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10900
10901 // Ensure shortcode does not get replaced when it is part of a word
10902 editor.handle_input(" Test:wave", window, cx);
10903 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
10904
10905 editor.handle_input(":", window, cx);
10906 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
10907
10908 editor.set_auto_replace_emoji_shortcode(false);
10909
10910 // Ensure shortcode does not get replaced when auto replace is off
10911 editor.handle_input(" :wave", window, cx);
10912 assert_eq!(
10913 editor.text(cx),
10914 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
10915 );
10916
10917 editor.handle_input(":", window, cx);
10918 assert_eq!(
10919 editor.text(cx),
10920 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
10921 );
10922 });
10923}
10924
10925#[gpui::test]
10926async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
10927 init_test(cx, |_| {});
10928
10929 let (text, insertion_ranges) = marked_text_ranges(
10930 indoc! {"
10931 ˇ
10932 "},
10933 false,
10934 );
10935
10936 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
10937 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10938
10939 _ = editor.update_in(cx, |editor, window, cx| {
10940 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
10941
10942 editor
10943 .insert_snippet(&insertion_ranges, snippet, window, cx)
10944 .unwrap();
10945
10946 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
10947 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
10948 assert_eq!(editor.text(cx), expected_text);
10949 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
10950 }
10951
10952 assert(
10953 editor,
10954 cx,
10955 indoc! {"
10956 type «» =•
10957 "},
10958 );
10959
10960 assert!(editor.context_menu_visible(), "There should be a matches");
10961 });
10962}
10963
10964#[gpui::test]
10965async fn test_snippets(cx: &mut TestAppContext) {
10966 init_test(cx, |_| {});
10967
10968 let mut cx = EditorTestContext::new(cx).await;
10969
10970 cx.set_state(indoc! {"
10971 a.ˇ b
10972 a.ˇ b
10973 a.ˇ b
10974 "});
10975
10976 cx.update_editor(|editor, window, cx| {
10977 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
10978 let insertion_ranges = editor
10979 .selections
10980 .all(cx)
10981 .iter()
10982 .map(|s| s.range())
10983 .collect::<Vec<_>>();
10984 editor
10985 .insert_snippet(&insertion_ranges, snippet, window, cx)
10986 .unwrap();
10987 });
10988
10989 cx.assert_editor_state(indoc! {"
10990 a.f(«oneˇ», two, «threeˇ») b
10991 a.f(«oneˇ», two, «threeˇ») b
10992 a.f(«oneˇ», two, «threeˇ») b
10993 "});
10994
10995 // Can't move earlier than the first tab stop
10996 cx.update_editor(|editor, window, cx| {
10997 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10998 });
10999 cx.assert_editor_state(indoc! {"
11000 a.f(«oneˇ», two, «threeˇ») b
11001 a.f(«oneˇ», two, «threeˇ») b
11002 a.f(«oneˇ», two, «threeˇ») b
11003 "});
11004
11005 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11006 cx.assert_editor_state(indoc! {"
11007 a.f(one, «twoˇ», three) b
11008 a.f(one, «twoˇ», three) b
11009 a.f(one, «twoˇ», three) b
11010 "});
11011
11012 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11013 cx.assert_editor_state(indoc! {"
11014 a.f(«oneˇ», two, «threeˇ») b
11015 a.f(«oneˇ», two, «threeˇ») b
11016 a.f(«oneˇ», two, «threeˇ») b
11017 "});
11018
11019 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11020 cx.assert_editor_state(indoc! {"
11021 a.f(one, «twoˇ», three) b
11022 a.f(one, «twoˇ», three) b
11023 a.f(one, «twoˇ», three) b
11024 "});
11025 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11026 cx.assert_editor_state(indoc! {"
11027 a.f(one, two, three)ˇ b
11028 a.f(one, two, three)ˇ b
11029 a.f(one, two, three)ˇ b
11030 "});
11031
11032 // As soon as the last tab stop is reached, snippet state is gone
11033 cx.update_editor(|editor, window, cx| {
11034 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11035 });
11036 cx.assert_editor_state(indoc! {"
11037 a.f(one, two, three)ˇ b
11038 a.f(one, two, three)ˇ b
11039 a.f(one, two, three)ˇ b
11040 "});
11041}
11042
11043#[gpui::test]
11044async fn test_snippet_indentation(cx: &mut TestAppContext) {
11045 init_test(cx, |_| {});
11046
11047 let mut cx = EditorTestContext::new(cx).await;
11048
11049 cx.update_editor(|editor, window, cx| {
11050 let snippet = Snippet::parse(indoc! {"
11051 /*
11052 * Multiline comment with leading indentation
11053 *
11054 * $1
11055 */
11056 $0"})
11057 .unwrap();
11058 let insertion_ranges = editor
11059 .selections
11060 .all(cx)
11061 .iter()
11062 .map(|s| s.range())
11063 .collect::<Vec<_>>();
11064 editor
11065 .insert_snippet(&insertion_ranges, snippet, window, cx)
11066 .unwrap();
11067 });
11068
11069 cx.assert_editor_state(indoc! {"
11070 /*
11071 * Multiline comment with leading indentation
11072 *
11073 * ˇ
11074 */
11075 "});
11076
11077 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11078 cx.assert_editor_state(indoc! {"
11079 /*
11080 * Multiline comment with leading indentation
11081 *
11082 *•
11083 */
11084 ˇ"});
11085}
11086
11087#[gpui::test]
11088async fn test_document_format_during_save(cx: &mut TestAppContext) {
11089 init_test(cx, |_| {});
11090
11091 let fs = FakeFs::new(cx.executor());
11092 fs.insert_file(path!("/file.rs"), Default::default()).await;
11093
11094 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11095
11096 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11097 language_registry.add(rust_lang());
11098 let mut fake_servers = language_registry.register_fake_lsp(
11099 "Rust",
11100 FakeLspAdapter {
11101 capabilities: lsp::ServerCapabilities {
11102 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11103 ..Default::default()
11104 },
11105 ..Default::default()
11106 },
11107 );
11108
11109 let buffer = project
11110 .update(cx, |project, cx| {
11111 project.open_local_buffer(path!("/file.rs"), cx)
11112 })
11113 .await
11114 .unwrap();
11115
11116 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11117 let (editor, cx) = cx.add_window_view(|window, cx| {
11118 build_editor_with_project(project.clone(), buffer, window, cx)
11119 });
11120 editor.update_in(cx, |editor, window, cx| {
11121 editor.set_text("one\ntwo\nthree\n", window, cx)
11122 });
11123 assert!(cx.read(|cx| editor.is_dirty(cx)));
11124
11125 cx.executor().start_waiting();
11126 let fake_server = fake_servers.next().await.unwrap();
11127
11128 {
11129 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11130 move |params, _| async move {
11131 assert_eq!(
11132 params.text_document.uri,
11133 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11134 );
11135 assert_eq!(params.options.tab_size, 4);
11136 Ok(Some(vec![lsp::TextEdit::new(
11137 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11138 ", ".to_string(),
11139 )]))
11140 },
11141 );
11142 let save = editor
11143 .update_in(cx, |editor, window, cx| {
11144 editor.save(
11145 SaveOptions {
11146 format: true,
11147 autosave: false,
11148 },
11149 project.clone(),
11150 window,
11151 cx,
11152 )
11153 })
11154 .unwrap();
11155 cx.executor().start_waiting();
11156 save.await;
11157
11158 assert_eq!(
11159 editor.update(cx, |editor, cx| editor.text(cx)),
11160 "one, two\nthree\n"
11161 );
11162 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11163 }
11164
11165 {
11166 editor.update_in(cx, |editor, window, cx| {
11167 editor.set_text("one\ntwo\nthree\n", window, cx)
11168 });
11169 assert!(cx.read(|cx| editor.is_dirty(cx)));
11170
11171 // Ensure we can still save even if formatting hangs.
11172 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11173 move |params, _| async move {
11174 assert_eq!(
11175 params.text_document.uri,
11176 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11177 );
11178 futures::future::pending::<()>().await;
11179 unreachable!()
11180 },
11181 );
11182 let save = editor
11183 .update_in(cx, |editor, window, cx| {
11184 editor.save(
11185 SaveOptions {
11186 format: true,
11187 autosave: false,
11188 },
11189 project.clone(),
11190 window,
11191 cx,
11192 )
11193 })
11194 .unwrap();
11195 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11196 cx.executor().start_waiting();
11197 save.await;
11198 assert_eq!(
11199 editor.update(cx, |editor, cx| editor.text(cx)),
11200 "one\ntwo\nthree\n"
11201 );
11202 }
11203
11204 // Set rust language override and assert overridden tabsize is sent to language server
11205 update_test_language_settings(cx, |settings| {
11206 settings.languages.0.insert(
11207 "Rust".into(),
11208 LanguageSettingsContent {
11209 tab_size: NonZeroU32::new(8),
11210 ..Default::default()
11211 },
11212 );
11213 });
11214
11215 {
11216 editor.update_in(cx, |editor, window, cx| {
11217 editor.set_text("somehting_new\n", window, cx)
11218 });
11219 assert!(cx.read(|cx| editor.is_dirty(cx)));
11220 let _formatting_request_signal = fake_server
11221 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11222 assert_eq!(
11223 params.text_document.uri,
11224 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11225 );
11226 assert_eq!(params.options.tab_size, 8);
11227 Ok(Some(vec![]))
11228 });
11229 let save = editor
11230 .update_in(cx, |editor, window, cx| {
11231 editor.save(
11232 SaveOptions {
11233 format: true,
11234 autosave: false,
11235 },
11236 project.clone(),
11237 window,
11238 cx,
11239 )
11240 })
11241 .unwrap();
11242 cx.executor().start_waiting();
11243 save.await;
11244 }
11245}
11246
11247#[gpui::test]
11248async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11249 init_test(cx, |settings| {
11250 settings.defaults.ensure_final_newline_on_save = Some(false);
11251 });
11252
11253 let fs = FakeFs::new(cx.executor());
11254 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11255
11256 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11257
11258 let buffer = project
11259 .update(cx, |project, cx| {
11260 project.open_local_buffer(path!("/file.txt"), cx)
11261 })
11262 .await
11263 .unwrap();
11264
11265 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11266 let (editor, cx) = cx.add_window_view(|window, cx| {
11267 build_editor_with_project(project.clone(), buffer, window, cx)
11268 });
11269 editor.update_in(cx, |editor, window, cx| {
11270 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11271 s.select_ranges([0..0])
11272 });
11273 });
11274 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11275
11276 editor.update_in(cx, |editor, window, cx| {
11277 editor.handle_input("\n", window, cx)
11278 });
11279 cx.run_until_parked();
11280 save(&editor, &project, cx).await;
11281 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11282
11283 editor.update_in(cx, |editor, window, cx| {
11284 editor.undo(&Default::default(), window, cx);
11285 });
11286 save(&editor, &project, cx).await;
11287 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11288
11289 editor.update_in(cx, |editor, window, cx| {
11290 editor.redo(&Default::default(), window, cx);
11291 });
11292 cx.run_until_parked();
11293 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11294
11295 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11296 let save = editor
11297 .update_in(cx, |editor, window, cx| {
11298 editor.save(
11299 SaveOptions {
11300 format: true,
11301 autosave: false,
11302 },
11303 project.clone(),
11304 window,
11305 cx,
11306 )
11307 })
11308 .unwrap();
11309 cx.executor().start_waiting();
11310 save.await;
11311 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11312 }
11313}
11314
11315#[gpui::test]
11316async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11317 init_test(cx, |_| {});
11318
11319 let cols = 4;
11320 let rows = 10;
11321 let sample_text_1 = sample_text(rows, cols, 'a');
11322 assert_eq!(
11323 sample_text_1,
11324 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11325 );
11326 let sample_text_2 = sample_text(rows, cols, 'l');
11327 assert_eq!(
11328 sample_text_2,
11329 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11330 );
11331 let sample_text_3 = sample_text(rows, cols, 'v');
11332 assert_eq!(
11333 sample_text_3,
11334 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11335 );
11336
11337 let fs = FakeFs::new(cx.executor());
11338 fs.insert_tree(
11339 path!("/a"),
11340 json!({
11341 "main.rs": sample_text_1,
11342 "other.rs": sample_text_2,
11343 "lib.rs": sample_text_3,
11344 }),
11345 )
11346 .await;
11347
11348 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11349 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11350 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11351
11352 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11353 language_registry.add(rust_lang());
11354 let mut fake_servers = language_registry.register_fake_lsp(
11355 "Rust",
11356 FakeLspAdapter {
11357 capabilities: lsp::ServerCapabilities {
11358 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11359 ..Default::default()
11360 },
11361 ..Default::default()
11362 },
11363 );
11364
11365 let worktree = project.update(cx, |project, cx| {
11366 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11367 assert_eq!(worktrees.len(), 1);
11368 worktrees.pop().unwrap()
11369 });
11370 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11371
11372 let buffer_1 = project
11373 .update(cx, |project, cx| {
11374 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11375 })
11376 .await
11377 .unwrap();
11378 let buffer_2 = project
11379 .update(cx, |project, cx| {
11380 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11381 })
11382 .await
11383 .unwrap();
11384 let buffer_3 = project
11385 .update(cx, |project, cx| {
11386 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11387 })
11388 .await
11389 .unwrap();
11390
11391 let multi_buffer = cx.new(|cx| {
11392 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11393 multi_buffer.push_excerpts(
11394 buffer_1.clone(),
11395 [
11396 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11397 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11398 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11399 ],
11400 cx,
11401 );
11402 multi_buffer.push_excerpts(
11403 buffer_2.clone(),
11404 [
11405 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11406 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11407 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11408 ],
11409 cx,
11410 );
11411 multi_buffer.push_excerpts(
11412 buffer_3.clone(),
11413 [
11414 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11415 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11416 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11417 ],
11418 cx,
11419 );
11420 multi_buffer
11421 });
11422 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11423 Editor::new(
11424 EditorMode::full(),
11425 multi_buffer,
11426 Some(project.clone()),
11427 window,
11428 cx,
11429 )
11430 });
11431
11432 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11433 editor.change_selections(
11434 SelectionEffects::scroll(Autoscroll::Next),
11435 window,
11436 cx,
11437 |s| s.select_ranges(Some(1..2)),
11438 );
11439 editor.insert("|one|two|three|", window, cx);
11440 });
11441 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11442 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11443 editor.change_selections(
11444 SelectionEffects::scroll(Autoscroll::Next),
11445 window,
11446 cx,
11447 |s| s.select_ranges(Some(60..70)),
11448 );
11449 editor.insert("|four|five|six|", window, cx);
11450 });
11451 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11452
11453 // First two buffers should be edited, but not the third one.
11454 assert_eq!(
11455 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11456 "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}",
11457 );
11458 buffer_1.update(cx, |buffer, _| {
11459 assert!(buffer.is_dirty());
11460 assert_eq!(
11461 buffer.text(),
11462 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11463 )
11464 });
11465 buffer_2.update(cx, |buffer, _| {
11466 assert!(buffer.is_dirty());
11467 assert_eq!(
11468 buffer.text(),
11469 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11470 )
11471 });
11472 buffer_3.update(cx, |buffer, _| {
11473 assert!(!buffer.is_dirty());
11474 assert_eq!(buffer.text(), sample_text_3,)
11475 });
11476 cx.executor().run_until_parked();
11477
11478 cx.executor().start_waiting();
11479 let save = multi_buffer_editor
11480 .update_in(cx, |editor, window, cx| {
11481 editor.save(
11482 SaveOptions {
11483 format: true,
11484 autosave: false,
11485 },
11486 project.clone(),
11487 window,
11488 cx,
11489 )
11490 })
11491 .unwrap();
11492
11493 let fake_server = fake_servers.next().await.unwrap();
11494 fake_server
11495 .server
11496 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11497 Ok(Some(vec![lsp::TextEdit::new(
11498 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11499 format!("[{} formatted]", params.text_document.uri),
11500 )]))
11501 })
11502 .detach();
11503 save.await;
11504
11505 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11506 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11507 assert_eq!(
11508 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11509 uri!(
11510 "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}"
11511 ),
11512 );
11513 buffer_1.update(cx, |buffer, _| {
11514 assert!(!buffer.is_dirty());
11515 assert_eq!(
11516 buffer.text(),
11517 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11518 )
11519 });
11520 buffer_2.update(cx, |buffer, _| {
11521 assert!(!buffer.is_dirty());
11522 assert_eq!(
11523 buffer.text(),
11524 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11525 )
11526 });
11527 buffer_3.update(cx, |buffer, _| {
11528 assert!(!buffer.is_dirty());
11529 assert_eq!(buffer.text(), sample_text_3,)
11530 });
11531}
11532
11533#[gpui::test]
11534async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11535 init_test(cx, |_| {});
11536
11537 let fs = FakeFs::new(cx.executor());
11538 fs.insert_tree(
11539 path!("/dir"),
11540 json!({
11541 "file1.rs": "fn main() { println!(\"hello\"); }",
11542 "file2.rs": "fn test() { println!(\"test\"); }",
11543 "file3.rs": "fn other() { println!(\"other\"); }\n",
11544 }),
11545 )
11546 .await;
11547
11548 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11549 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11550 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11551
11552 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11553 language_registry.add(rust_lang());
11554
11555 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11556 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11557
11558 // Open three buffers
11559 let buffer_1 = project
11560 .update(cx, |project, cx| {
11561 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11562 })
11563 .await
11564 .unwrap();
11565 let buffer_2 = project
11566 .update(cx, |project, cx| {
11567 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11568 })
11569 .await
11570 .unwrap();
11571 let buffer_3 = project
11572 .update(cx, |project, cx| {
11573 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11574 })
11575 .await
11576 .unwrap();
11577
11578 // Create a multi-buffer with all three buffers
11579 let multi_buffer = cx.new(|cx| {
11580 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11581 multi_buffer.push_excerpts(
11582 buffer_1.clone(),
11583 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11584 cx,
11585 );
11586 multi_buffer.push_excerpts(
11587 buffer_2.clone(),
11588 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11589 cx,
11590 );
11591 multi_buffer.push_excerpts(
11592 buffer_3.clone(),
11593 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11594 cx,
11595 );
11596 multi_buffer
11597 });
11598
11599 let editor = cx.new_window_entity(|window, cx| {
11600 Editor::new(
11601 EditorMode::full(),
11602 multi_buffer,
11603 Some(project.clone()),
11604 window,
11605 cx,
11606 )
11607 });
11608
11609 // Edit only the first buffer
11610 editor.update_in(cx, |editor, window, cx| {
11611 editor.change_selections(
11612 SelectionEffects::scroll(Autoscroll::Next),
11613 window,
11614 cx,
11615 |s| s.select_ranges(Some(10..10)),
11616 );
11617 editor.insert("// edited", window, cx);
11618 });
11619
11620 // Verify that only buffer 1 is dirty
11621 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11622 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11623 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11624
11625 // Get write counts after file creation (files were created with initial content)
11626 // We expect each file to have been written once during creation
11627 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11628 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11629 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11630
11631 // Perform autosave
11632 let save_task = editor.update_in(cx, |editor, window, cx| {
11633 editor.save(
11634 SaveOptions {
11635 format: true,
11636 autosave: true,
11637 },
11638 project.clone(),
11639 window,
11640 cx,
11641 )
11642 });
11643 save_task.await.unwrap();
11644
11645 // Only the dirty buffer should have been saved
11646 assert_eq!(
11647 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11648 1,
11649 "Buffer 1 was dirty, so it should have been written once during autosave"
11650 );
11651 assert_eq!(
11652 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11653 0,
11654 "Buffer 2 was clean, so it should not have been written during autosave"
11655 );
11656 assert_eq!(
11657 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11658 0,
11659 "Buffer 3 was clean, so it should not have been written during autosave"
11660 );
11661
11662 // Verify buffer states after autosave
11663 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11664 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11665 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11666
11667 // Now perform a manual save (format = true)
11668 let save_task = editor.update_in(cx, |editor, window, cx| {
11669 editor.save(
11670 SaveOptions {
11671 format: true,
11672 autosave: false,
11673 },
11674 project.clone(),
11675 window,
11676 cx,
11677 )
11678 });
11679 save_task.await.unwrap();
11680
11681 // During manual save, clean buffers don't get written to disk
11682 // They just get did_save called for language server notifications
11683 assert_eq!(
11684 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11685 1,
11686 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11687 );
11688 assert_eq!(
11689 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11690 0,
11691 "Buffer 2 should not have been written at all"
11692 );
11693 assert_eq!(
11694 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11695 0,
11696 "Buffer 3 should not have been written at all"
11697 );
11698}
11699
11700async fn setup_range_format_test(
11701 cx: &mut TestAppContext,
11702) -> (
11703 Entity<Project>,
11704 Entity<Editor>,
11705 &mut gpui::VisualTestContext,
11706 lsp::FakeLanguageServer,
11707) {
11708 init_test(cx, |_| {});
11709
11710 let fs = FakeFs::new(cx.executor());
11711 fs.insert_file(path!("/file.rs"), Default::default()).await;
11712
11713 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11714
11715 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11716 language_registry.add(rust_lang());
11717 let mut fake_servers = language_registry.register_fake_lsp(
11718 "Rust",
11719 FakeLspAdapter {
11720 capabilities: lsp::ServerCapabilities {
11721 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11722 ..lsp::ServerCapabilities::default()
11723 },
11724 ..FakeLspAdapter::default()
11725 },
11726 );
11727
11728 let buffer = project
11729 .update(cx, |project, cx| {
11730 project.open_local_buffer(path!("/file.rs"), cx)
11731 })
11732 .await
11733 .unwrap();
11734
11735 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11736 let (editor, cx) = cx.add_window_view(|window, cx| {
11737 build_editor_with_project(project.clone(), buffer, window, cx)
11738 });
11739
11740 cx.executor().start_waiting();
11741 let fake_server = fake_servers.next().await.unwrap();
11742
11743 (project, editor, cx, fake_server)
11744}
11745
11746#[gpui::test]
11747async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11748 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11749
11750 editor.update_in(cx, |editor, window, cx| {
11751 editor.set_text("one\ntwo\nthree\n", window, cx)
11752 });
11753 assert!(cx.read(|cx| editor.is_dirty(cx)));
11754
11755 let save = editor
11756 .update_in(cx, |editor, window, cx| {
11757 editor.save(
11758 SaveOptions {
11759 format: true,
11760 autosave: false,
11761 },
11762 project.clone(),
11763 window,
11764 cx,
11765 )
11766 })
11767 .unwrap();
11768 fake_server
11769 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11770 assert_eq!(
11771 params.text_document.uri,
11772 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11773 );
11774 assert_eq!(params.options.tab_size, 4);
11775 Ok(Some(vec![lsp::TextEdit::new(
11776 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11777 ", ".to_string(),
11778 )]))
11779 })
11780 .next()
11781 .await;
11782 cx.executor().start_waiting();
11783 save.await;
11784 assert_eq!(
11785 editor.update(cx, |editor, cx| editor.text(cx)),
11786 "one, two\nthree\n"
11787 );
11788 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11789}
11790
11791#[gpui::test]
11792async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11793 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11794
11795 editor.update_in(cx, |editor, window, cx| {
11796 editor.set_text("one\ntwo\nthree\n", window, cx)
11797 });
11798 assert!(cx.read(|cx| editor.is_dirty(cx)));
11799
11800 // Test that save still works when formatting hangs
11801 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11802 move |params, _| async move {
11803 assert_eq!(
11804 params.text_document.uri,
11805 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11806 );
11807 futures::future::pending::<()>().await;
11808 unreachable!()
11809 },
11810 );
11811 let save = editor
11812 .update_in(cx, |editor, window, cx| {
11813 editor.save(
11814 SaveOptions {
11815 format: true,
11816 autosave: false,
11817 },
11818 project.clone(),
11819 window,
11820 cx,
11821 )
11822 })
11823 .unwrap();
11824 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11825 cx.executor().start_waiting();
11826 save.await;
11827 assert_eq!(
11828 editor.update(cx, |editor, cx| editor.text(cx)),
11829 "one\ntwo\nthree\n"
11830 );
11831 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11832}
11833
11834#[gpui::test]
11835async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11836 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11837
11838 // Buffer starts clean, no formatting should be requested
11839 let save = editor
11840 .update_in(cx, |editor, window, cx| {
11841 editor.save(
11842 SaveOptions {
11843 format: false,
11844 autosave: false,
11845 },
11846 project.clone(),
11847 window,
11848 cx,
11849 )
11850 })
11851 .unwrap();
11852 let _pending_format_request = fake_server
11853 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11854 panic!("Should not be invoked");
11855 })
11856 .next();
11857 cx.executor().start_waiting();
11858 save.await;
11859 cx.run_until_parked();
11860}
11861
11862#[gpui::test]
11863async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11864 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11865
11866 // Set Rust language override and assert overridden tabsize is sent to language server
11867 update_test_language_settings(cx, |settings| {
11868 settings.languages.0.insert(
11869 "Rust".into(),
11870 LanguageSettingsContent {
11871 tab_size: NonZeroU32::new(8),
11872 ..Default::default()
11873 },
11874 );
11875 });
11876
11877 editor.update_in(cx, |editor, window, cx| {
11878 editor.set_text("something_new\n", window, cx)
11879 });
11880 assert!(cx.read(|cx| editor.is_dirty(cx)));
11881 let save = editor
11882 .update_in(cx, |editor, window, cx| {
11883 editor.save(
11884 SaveOptions {
11885 format: true,
11886 autosave: false,
11887 },
11888 project.clone(),
11889 window,
11890 cx,
11891 )
11892 })
11893 .unwrap();
11894 fake_server
11895 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11896 assert_eq!(
11897 params.text_document.uri,
11898 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11899 );
11900 assert_eq!(params.options.tab_size, 8);
11901 Ok(Some(Vec::new()))
11902 })
11903 .next()
11904 .await;
11905 save.await;
11906}
11907
11908#[gpui::test]
11909async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
11910 init_test(cx, |settings| {
11911 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
11912 Formatter::LanguageServer { name: None },
11913 )))
11914 });
11915
11916 let fs = FakeFs::new(cx.executor());
11917 fs.insert_file(path!("/file.rs"), Default::default()).await;
11918
11919 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11920
11921 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11922 language_registry.add(Arc::new(Language::new(
11923 LanguageConfig {
11924 name: "Rust".into(),
11925 matcher: LanguageMatcher {
11926 path_suffixes: vec!["rs".to_string()],
11927 ..Default::default()
11928 },
11929 ..LanguageConfig::default()
11930 },
11931 Some(tree_sitter_rust::LANGUAGE.into()),
11932 )));
11933 update_test_language_settings(cx, |settings| {
11934 // Enable Prettier formatting for the same buffer, and ensure
11935 // LSP is called instead of Prettier.
11936 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
11937 });
11938 let mut fake_servers = language_registry.register_fake_lsp(
11939 "Rust",
11940 FakeLspAdapter {
11941 capabilities: lsp::ServerCapabilities {
11942 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11943 ..Default::default()
11944 },
11945 ..Default::default()
11946 },
11947 );
11948
11949 let buffer = project
11950 .update(cx, |project, cx| {
11951 project.open_local_buffer(path!("/file.rs"), cx)
11952 })
11953 .await
11954 .unwrap();
11955
11956 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11957 let (editor, cx) = cx.add_window_view(|window, cx| {
11958 build_editor_with_project(project.clone(), buffer, window, cx)
11959 });
11960 editor.update_in(cx, |editor, window, cx| {
11961 editor.set_text("one\ntwo\nthree\n", window, cx)
11962 });
11963
11964 cx.executor().start_waiting();
11965 let fake_server = fake_servers.next().await.unwrap();
11966
11967 let format = editor
11968 .update_in(cx, |editor, window, cx| {
11969 editor.perform_format(
11970 project.clone(),
11971 FormatTrigger::Manual,
11972 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11973 window,
11974 cx,
11975 )
11976 })
11977 .unwrap();
11978 fake_server
11979 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11980 assert_eq!(
11981 params.text_document.uri,
11982 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11983 );
11984 assert_eq!(params.options.tab_size, 4);
11985 Ok(Some(vec![lsp::TextEdit::new(
11986 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11987 ", ".to_string(),
11988 )]))
11989 })
11990 .next()
11991 .await;
11992 cx.executor().start_waiting();
11993 format.await;
11994 assert_eq!(
11995 editor.update(cx, |editor, cx| editor.text(cx)),
11996 "one, two\nthree\n"
11997 );
11998
11999 editor.update_in(cx, |editor, window, cx| {
12000 editor.set_text("one\ntwo\nthree\n", window, cx)
12001 });
12002 // Ensure we don't lock if formatting hangs.
12003 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12004 move |params, _| async move {
12005 assert_eq!(
12006 params.text_document.uri,
12007 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12008 );
12009 futures::future::pending::<()>().await;
12010 unreachable!()
12011 },
12012 );
12013 let format = editor
12014 .update_in(cx, |editor, window, cx| {
12015 editor.perform_format(
12016 project,
12017 FormatTrigger::Manual,
12018 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12019 window,
12020 cx,
12021 )
12022 })
12023 .unwrap();
12024 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12025 cx.executor().start_waiting();
12026 format.await;
12027 assert_eq!(
12028 editor.update(cx, |editor, cx| editor.text(cx)),
12029 "one\ntwo\nthree\n"
12030 );
12031}
12032
12033#[gpui::test]
12034async fn test_multiple_formatters(cx: &mut TestAppContext) {
12035 init_test(cx, |settings| {
12036 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12037 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
12038 Formatter::LanguageServer { name: None },
12039 Formatter::CodeAction("code-action-1".into()),
12040 Formatter::CodeAction("code-action-2".into()),
12041 ])))
12042 });
12043
12044 let fs = FakeFs::new(cx.executor());
12045 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12046 .await;
12047
12048 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12049 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12050 language_registry.add(rust_lang());
12051
12052 let mut fake_servers = language_registry.register_fake_lsp(
12053 "Rust",
12054 FakeLspAdapter {
12055 capabilities: lsp::ServerCapabilities {
12056 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12057 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12058 commands: vec!["the-command-for-code-action-1".into()],
12059 ..Default::default()
12060 }),
12061 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12062 ..Default::default()
12063 },
12064 ..Default::default()
12065 },
12066 );
12067
12068 let buffer = project
12069 .update(cx, |project, cx| {
12070 project.open_local_buffer(path!("/file.rs"), cx)
12071 })
12072 .await
12073 .unwrap();
12074
12075 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12076 let (editor, cx) = cx.add_window_view(|window, cx| {
12077 build_editor_with_project(project.clone(), buffer, window, cx)
12078 });
12079
12080 cx.executor().start_waiting();
12081
12082 let fake_server = fake_servers.next().await.unwrap();
12083 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12084 move |_params, _| async move {
12085 Ok(Some(vec![lsp::TextEdit::new(
12086 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12087 "applied-formatting\n".to_string(),
12088 )]))
12089 },
12090 );
12091 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12092 move |params, _| async move {
12093 let requested_code_actions = params.context.only.expect("Expected code action request");
12094 assert_eq!(requested_code_actions.len(), 1);
12095
12096 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12097 let code_action = match requested_code_actions[0].as_str() {
12098 "code-action-1" => lsp::CodeAction {
12099 kind: Some("code-action-1".into()),
12100 edit: Some(lsp::WorkspaceEdit::new(
12101 [(
12102 uri,
12103 vec![lsp::TextEdit::new(
12104 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12105 "applied-code-action-1-edit\n".to_string(),
12106 )],
12107 )]
12108 .into_iter()
12109 .collect(),
12110 )),
12111 command: Some(lsp::Command {
12112 command: "the-command-for-code-action-1".into(),
12113 ..Default::default()
12114 }),
12115 ..Default::default()
12116 },
12117 "code-action-2" => lsp::CodeAction {
12118 kind: Some("code-action-2".into()),
12119 edit: Some(lsp::WorkspaceEdit::new(
12120 [(
12121 uri,
12122 vec![lsp::TextEdit::new(
12123 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12124 "applied-code-action-2-edit\n".to_string(),
12125 )],
12126 )]
12127 .into_iter()
12128 .collect(),
12129 )),
12130 ..Default::default()
12131 },
12132 req => panic!("Unexpected code action request: {:?}", req),
12133 };
12134 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12135 code_action,
12136 )]))
12137 },
12138 );
12139
12140 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12141 move |params, _| async move { Ok(params) }
12142 });
12143
12144 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12145 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12146 let fake = fake_server.clone();
12147 let lock = command_lock.clone();
12148 move |params, _| {
12149 assert_eq!(params.command, "the-command-for-code-action-1");
12150 let fake = fake.clone();
12151 let lock = lock.clone();
12152 async move {
12153 lock.lock().await;
12154 fake.server
12155 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12156 label: None,
12157 edit: lsp::WorkspaceEdit {
12158 changes: Some(
12159 [(
12160 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12161 vec![lsp::TextEdit {
12162 range: lsp::Range::new(
12163 lsp::Position::new(0, 0),
12164 lsp::Position::new(0, 0),
12165 ),
12166 new_text: "applied-code-action-1-command\n".into(),
12167 }],
12168 )]
12169 .into_iter()
12170 .collect(),
12171 ),
12172 ..Default::default()
12173 },
12174 })
12175 .await
12176 .into_response()
12177 .unwrap();
12178 Ok(Some(json!(null)))
12179 }
12180 }
12181 });
12182
12183 cx.executor().start_waiting();
12184 editor
12185 .update_in(cx, |editor, window, cx| {
12186 editor.perform_format(
12187 project.clone(),
12188 FormatTrigger::Manual,
12189 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12190 window,
12191 cx,
12192 )
12193 })
12194 .unwrap()
12195 .await;
12196 editor.update(cx, |editor, cx| {
12197 assert_eq!(
12198 editor.text(cx),
12199 r#"
12200 applied-code-action-2-edit
12201 applied-code-action-1-command
12202 applied-code-action-1-edit
12203 applied-formatting
12204 one
12205 two
12206 three
12207 "#
12208 .unindent()
12209 );
12210 });
12211
12212 editor.update_in(cx, |editor, window, cx| {
12213 editor.undo(&Default::default(), window, cx);
12214 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12215 });
12216
12217 // Perform a manual edit while waiting for an LSP command
12218 // that's being run as part of a formatting code action.
12219 let lock_guard = command_lock.lock().await;
12220 let format = editor
12221 .update_in(cx, |editor, window, cx| {
12222 editor.perform_format(
12223 project.clone(),
12224 FormatTrigger::Manual,
12225 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12226 window,
12227 cx,
12228 )
12229 })
12230 .unwrap();
12231 cx.run_until_parked();
12232 editor.update(cx, |editor, cx| {
12233 assert_eq!(
12234 editor.text(cx),
12235 r#"
12236 applied-code-action-1-edit
12237 applied-formatting
12238 one
12239 two
12240 three
12241 "#
12242 .unindent()
12243 );
12244
12245 editor.buffer.update(cx, |buffer, cx| {
12246 let ix = buffer.len(cx);
12247 buffer.edit([(ix..ix, "edited\n")], None, cx);
12248 });
12249 });
12250
12251 // Allow the LSP command to proceed. Because the buffer was edited,
12252 // the second code action will not be run.
12253 drop(lock_guard);
12254 format.await;
12255 editor.update_in(cx, |editor, window, cx| {
12256 assert_eq!(
12257 editor.text(cx),
12258 r#"
12259 applied-code-action-1-command
12260 applied-code-action-1-edit
12261 applied-formatting
12262 one
12263 two
12264 three
12265 edited
12266 "#
12267 .unindent()
12268 );
12269
12270 // The manual edit is undone first, because it is the last thing the user did
12271 // (even though the command completed afterwards).
12272 editor.undo(&Default::default(), window, cx);
12273 assert_eq!(
12274 editor.text(cx),
12275 r#"
12276 applied-code-action-1-command
12277 applied-code-action-1-edit
12278 applied-formatting
12279 one
12280 two
12281 three
12282 "#
12283 .unindent()
12284 );
12285
12286 // All the formatting (including the command, which completed after the manual edit)
12287 // is undone together.
12288 editor.undo(&Default::default(), window, cx);
12289 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12290 });
12291}
12292
12293#[gpui::test]
12294async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12295 init_test(cx, |settings| {
12296 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
12297 Formatter::LanguageServer { name: None },
12298 ])))
12299 });
12300
12301 let fs = FakeFs::new(cx.executor());
12302 fs.insert_file(path!("/file.ts"), Default::default()).await;
12303
12304 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12305
12306 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12307 language_registry.add(Arc::new(Language::new(
12308 LanguageConfig {
12309 name: "TypeScript".into(),
12310 matcher: LanguageMatcher {
12311 path_suffixes: vec!["ts".to_string()],
12312 ..Default::default()
12313 },
12314 ..LanguageConfig::default()
12315 },
12316 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12317 )));
12318 update_test_language_settings(cx, |settings| {
12319 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12320 });
12321 let mut fake_servers = language_registry.register_fake_lsp(
12322 "TypeScript",
12323 FakeLspAdapter {
12324 capabilities: lsp::ServerCapabilities {
12325 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12326 ..Default::default()
12327 },
12328 ..Default::default()
12329 },
12330 );
12331
12332 let buffer = project
12333 .update(cx, |project, cx| {
12334 project.open_local_buffer(path!("/file.ts"), cx)
12335 })
12336 .await
12337 .unwrap();
12338
12339 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12340 let (editor, cx) = cx.add_window_view(|window, cx| {
12341 build_editor_with_project(project.clone(), buffer, window, cx)
12342 });
12343 editor.update_in(cx, |editor, window, cx| {
12344 editor.set_text(
12345 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12346 window,
12347 cx,
12348 )
12349 });
12350
12351 cx.executor().start_waiting();
12352 let fake_server = fake_servers.next().await.unwrap();
12353
12354 let format = editor
12355 .update_in(cx, |editor, window, cx| {
12356 editor.perform_code_action_kind(
12357 project.clone(),
12358 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12359 window,
12360 cx,
12361 )
12362 })
12363 .unwrap();
12364 fake_server
12365 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12366 assert_eq!(
12367 params.text_document.uri,
12368 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12369 );
12370 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12371 lsp::CodeAction {
12372 title: "Organize Imports".to_string(),
12373 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12374 edit: Some(lsp::WorkspaceEdit {
12375 changes: Some(
12376 [(
12377 params.text_document.uri.clone(),
12378 vec![lsp::TextEdit::new(
12379 lsp::Range::new(
12380 lsp::Position::new(1, 0),
12381 lsp::Position::new(2, 0),
12382 ),
12383 "".to_string(),
12384 )],
12385 )]
12386 .into_iter()
12387 .collect(),
12388 ),
12389 ..Default::default()
12390 }),
12391 ..Default::default()
12392 },
12393 )]))
12394 })
12395 .next()
12396 .await;
12397 cx.executor().start_waiting();
12398 format.await;
12399 assert_eq!(
12400 editor.update(cx, |editor, cx| editor.text(cx)),
12401 "import { a } from 'module';\n\nconst x = a;\n"
12402 );
12403
12404 editor.update_in(cx, |editor, window, cx| {
12405 editor.set_text(
12406 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12407 window,
12408 cx,
12409 )
12410 });
12411 // Ensure we don't lock if code action hangs.
12412 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12413 move |params, _| async move {
12414 assert_eq!(
12415 params.text_document.uri,
12416 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12417 );
12418 futures::future::pending::<()>().await;
12419 unreachable!()
12420 },
12421 );
12422 let format = editor
12423 .update_in(cx, |editor, window, cx| {
12424 editor.perform_code_action_kind(
12425 project,
12426 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12427 window,
12428 cx,
12429 )
12430 })
12431 .unwrap();
12432 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12433 cx.executor().start_waiting();
12434 format.await;
12435 assert_eq!(
12436 editor.update(cx, |editor, cx| editor.text(cx)),
12437 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12438 );
12439}
12440
12441#[gpui::test]
12442async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12443 init_test(cx, |_| {});
12444
12445 let mut cx = EditorLspTestContext::new_rust(
12446 lsp::ServerCapabilities {
12447 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12448 ..Default::default()
12449 },
12450 cx,
12451 )
12452 .await;
12453
12454 cx.set_state(indoc! {"
12455 one.twoˇ
12456 "});
12457
12458 // The format request takes a long time. When it completes, it inserts
12459 // a newline and an indent before the `.`
12460 cx.lsp
12461 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12462 let executor = cx.background_executor().clone();
12463 async move {
12464 executor.timer(Duration::from_millis(100)).await;
12465 Ok(Some(vec![lsp::TextEdit {
12466 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12467 new_text: "\n ".into(),
12468 }]))
12469 }
12470 });
12471
12472 // Submit a format request.
12473 let format_1 = cx
12474 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12475 .unwrap();
12476 cx.executor().run_until_parked();
12477
12478 // Submit a second format request.
12479 let format_2 = cx
12480 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12481 .unwrap();
12482 cx.executor().run_until_parked();
12483
12484 // Wait for both format requests to complete
12485 cx.executor().advance_clock(Duration::from_millis(200));
12486 cx.executor().start_waiting();
12487 format_1.await.unwrap();
12488 cx.executor().start_waiting();
12489 format_2.await.unwrap();
12490
12491 // The formatting edits only happens once.
12492 cx.assert_editor_state(indoc! {"
12493 one
12494 .twoˇ
12495 "});
12496}
12497
12498#[gpui::test]
12499async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12500 init_test(cx, |settings| {
12501 settings.defaults.formatter = Some(SelectedFormatter::Auto)
12502 });
12503
12504 let mut cx = EditorLspTestContext::new_rust(
12505 lsp::ServerCapabilities {
12506 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12507 ..Default::default()
12508 },
12509 cx,
12510 )
12511 .await;
12512
12513 // Set up a buffer white some trailing whitespace and no trailing newline.
12514 cx.set_state(
12515 &[
12516 "one ", //
12517 "twoˇ", //
12518 "three ", //
12519 "four", //
12520 ]
12521 .join("\n"),
12522 );
12523
12524 // Record which buffer changes have been sent to the language server
12525 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12526 cx.lsp
12527 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12528 let buffer_changes = buffer_changes.clone();
12529 move |params, _| {
12530 buffer_changes.lock().extend(
12531 params
12532 .content_changes
12533 .into_iter()
12534 .map(|e| (e.range.unwrap(), e.text)),
12535 );
12536 }
12537 });
12538
12539 // Handle formatting requests to the language server.
12540 cx.lsp
12541 .set_request_handler::<lsp::request::Formatting, _, _>({
12542 let buffer_changes = buffer_changes.clone();
12543 move |_, _| {
12544 let buffer_changes = buffer_changes.clone();
12545 // Insert blank lines between each line of the buffer.
12546 async move {
12547 // When formatting is requested, trailing whitespace has already been stripped,
12548 // and the trailing newline has already been added.
12549 assert_eq!(
12550 &buffer_changes.lock()[1..],
12551 &[
12552 (
12553 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12554 "".into()
12555 ),
12556 (
12557 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12558 "".into()
12559 ),
12560 (
12561 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12562 "\n".into()
12563 ),
12564 ]
12565 );
12566
12567 Ok(Some(vec![
12568 lsp::TextEdit {
12569 range: lsp::Range::new(
12570 lsp::Position::new(1, 0),
12571 lsp::Position::new(1, 0),
12572 ),
12573 new_text: "\n".into(),
12574 },
12575 lsp::TextEdit {
12576 range: lsp::Range::new(
12577 lsp::Position::new(2, 0),
12578 lsp::Position::new(2, 0),
12579 ),
12580 new_text: "\n".into(),
12581 },
12582 ]))
12583 }
12584 }
12585 });
12586
12587 // Submit a format request.
12588 let format = cx
12589 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12590 .unwrap();
12591
12592 cx.run_until_parked();
12593 // After formatting the buffer, the trailing whitespace is stripped,
12594 // a newline is appended, and the edits provided by the language server
12595 // have been applied.
12596 format.await.unwrap();
12597
12598 cx.assert_editor_state(
12599 &[
12600 "one", //
12601 "", //
12602 "twoˇ", //
12603 "", //
12604 "three", //
12605 "four", //
12606 "", //
12607 ]
12608 .join("\n"),
12609 );
12610
12611 // Undoing the formatting undoes the trailing whitespace removal, the
12612 // trailing newline, and the LSP edits.
12613 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12614 cx.assert_editor_state(
12615 &[
12616 "one ", //
12617 "twoˇ", //
12618 "three ", //
12619 "four", //
12620 ]
12621 .join("\n"),
12622 );
12623}
12624
12625#[gpui::test]
12626async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12627 cx: &mut TestAppContext,
12628) {
12629 init_test(cx, |_| {});
12630
12631 cx.update(|cx| {
12632 cx.update_global::<SettingsStore, _>(|settings, cx| {
12633 settings.update_user_settings(cx, |settings| {
12634 settings.editor.auto_signature_help = Some(true);
12635 });
12636 });
12637 });
12638
12639 let mut cx = EditorLspTestContext::new_rust(
12640 lsp::ServerCapabilities {
12641 signature_help_provider: Some(lsp::SignatureHelpOptions {
12642 ..Default::default()
12643 }),
12644 ..Default::default()
12645 },
12646 cx,
12647 )
12648 .await;
12649
12650 let language = Language::new(
12651 LanguageConfig {
12652 name: "Rust".into(),
12653 brackets: BracketPairConfig {
12654 pairs: vec![
12655 BracketPair {
12656 start: "{".to_string(),
12657 end: "}".to_string(),
12658 close: true,
12659 surround: true,
12660 newline: true,
12661 },
12662 BracketPair {
12663 start: "(".to_string(),
12664 end: ")".to_string(),
12665 close: true,
12666 surround: true,
12667 newline: true,
12668 },
12669 BracketPair {
12670 start: "/*".to_string(),
12671 end: " */".to_string(),
12672 close: true,
12673 surround: true,
12674 newline: true,
12675 },
12676 BracketPair {
12677 start: "[".to_string(),
12678 end: "]".to_string(),
12679 close: false,
12680 surround: false,
12681 newline: true,
12682 },
12683 BracketPair {
12684 start: "\"".to_string(),
12685 end: "\"".to_string(),
12686 close: true,
12687 surround: true,
12688 newline: false,
12689 },
12690 BracketPair {
12691 start: "<".to_string(),
12692 end: ">".to_string(),
12693 close: false,
12694 surround: true,
12695 newline: true,
12696 },
12697 ],
12698 ..Default::default()
12699 },
12700 autoclose_before: "})]".to_string(),
12701 ..Default::default()
12702 },
12703 Some(tree_sitter_rust::LANGUAGE.into()),
12704 );
12705 let language = Arc::new(language);
12706
12707 cx.language_registry().add(language.clone());
12708 cx.update_buffer(|buffer, cx| {
12709 buffer.set_language(Some(language), cx);
12710 });
12711
12712 cx.set_state(
12713 &r#"
12714 fn main() {
12715 sampleˇ
12716 }
12717 "#
12718 .unindent(),
12719 );
12720
12721 cx.update_editor(|editor, window, cx| {
12722 editor.handle_input("(", window, cx);
12723 });
12724 cx.assert_editor_state(
12725 &"
12726 fn main() {
12727 sample(ˇ)
12728 }
12729 "
12730 .unindent(),
12731 );
12732
12733 let mocked_response = lsp::SignatureHelp {
12734 signatures: vec![lsp::SignatureInformation {
12735 label: "fn sample(param1: u8, param2: u8)".to_string(),
12736 documentation: None,
12737 parameters: Some(vec![
12738 lsp::ParameterInformation {
12739 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12740 documentation: None,
12741 },
12742 lsp::ParameterInformation {
12743 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12744 documentation: None,
12745 },
12746 ]),
12747 active_parameter: None,
12748 }],
12749 active_signature: Some(0),
12750 active_parameter: Some(0),
12751 };
12752 handle_signature_help_request(&mut cx, mocked_response).await;
12753
12754 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12755 .await;
12756
12757 cx.editor(|editor, _, _| {
12758 let signature_help_state = editor.signature_help_state.popover().cloned();
12759 let signature = signature_help_state.unwrap();
12760 assert_eq!(
12761 signature.signatures[signature.current_signature].label,
12762 "fn sample(param1: u8, param2: u8)"
12763 );
12764 });
12765}
12766
12767#[gpui::test]
12768async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12769 init_test(cx, |_| {});
12770
12771 cx.update(|cx| {
12772 cx.update_global::<SettingsStore, _>(|settings, cx| {
12773 settings.update_user_settings(cx, |settings| {
12774 settings.editor.auto_signature_help = Some(false);
12775 settings.editor.show_signature_help_after_edits = Some(false);
12776 });
12777 });
12778 });
12779
12780 let mut cx = EditorLspTestContext::new_rust(
12781 lsp::ServerCapabilities {
12782 signature_help_provider: Some(lsp::SignatureHelpOptions {
12783 ..Default::default()
12784 }),
12785 ..Default::default()
12786 },
12787 cx,
12788 )
12789 .await;
12790
12791 let language = Language::new(
12792 LanguageConfig {
12793 name: "Rust".into(),
12794 brackets: BracketPairConfig {
12795 pairs: vec![
12796 BracketPair {
12797 start: "{".to_string(),
12798 end: "}".to_string(),
12799 close: true,
12800 surround: true,
12801 newline: true,
12802 },
12803 BracketPair {
12804 start: "(".to_string(),
12805 end: ")".to_string(),
12806 close: true,
12807 surround: true,
12808 newline: true,
12809 },
12810 BracketPair {
12811 start: "/*".to_string(),
12812 end: " */".to_string(),
12813 close: true,
12814 surround: true,
12815 newline: true,
12816 },
12817 BracketPair {
12818 start: "[".to_string(),
12819 end: "]".to_string(),
12820 close: false,
12821 surround: false,
12822 newline: true,
12823 },
12824 BracketPair {
12825 start: "\"".to_string(),
12826 end: "\"".to_string(),
12827 close: true,
12828 surround: true,
12829 newline: false,
12830 },
12831 BracketPair {
12832 start: "<".to_string(),
12833 end: ">".to_string(),
12834 close: false,
12835 surround: true,
12836 newline: true,
12837 },
12838 ],
12839 ..Default::default()
12840 },
12841 autoclose_before: "})]".to_string(),
12842 ..Default::default()
12843 },
12844 Some(tree_sitter_rust::LANGUAGE.into()),
12845 );
12846 let language = Arc::new(language);
12847
12848 cx.language_registry().add(language.clone());
12849 cx.update_buffer(|buffer, cx| {
12850 buffer.set_language(Some(language), cx);
12851 });
12852
12853 // Ensure that signature_help is not called when no signature help is enabled.
12854 cx.set_state(
12855 &r#"
12856 fn main() {
12857 sampleˇ
12858 }
12859 "#
12860 .unindent(),
12861 );
12862 cx.update_editor(|editor, window, cx| {
12863 editor.handle_input("(", window, cx);
12864 });
12865 cx.assert_editor_state(
12866 &"
12867 fn main() {
12868 sample(ˇ)
12869 }
12870 "
12871 .unindent(),
12872 );
12873 cx.editor(|editor, _, _| {
12874 assert!(editor.signature_help_state.task().is_none());
12875 });
12876
12877 let mocked_response = lsp::SignatureHelp {
12878 signatures: vec![lsp::SignatureInformation {
12879 label: "fn sample(param1: u8, param2: u8)".to_string(),
12880 documentation: None,
12881 parameters: Some(vec![
12882 lsp::ParameterInformation {
12883 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12884 documentation: None,
12885 },
12886 lsp::ParameterInformation {
12887 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12888 documentation: None,
12889 },
12890 ]),
12891 active_parameter: None,
12892 }],
12893 active_signature: Some(0),
12894 active_parameter: Some(0),
12895 };
12896
12897 // Ensure that signature_help is called when enabled afte edits
12898 cx.update(|_, cx| {
12899 cx.update_global::<SettingsStore, _>(|settings, cx| {
12900 settings.update_user_settings(cx, |settings| {
12901 settings.editor.auto_signature_help = Some(false);
12902 settings.editor.show_signature_help_after_edits = Some(true);
12903 });
12904 });
12905 });
12906 cx.set_state(
12907 &r#"
12908 fn main() {
12909 sampleˇ
12910 }
12911 "#
12912 .unindent(),
12913 );
12914 cx.update_editor(|editor, window, cx| {
12915 editor.handle_input("(", window, cx);
12916 });
12917 cx.assert_editor_state(
12918 &"
12919 fn main() {
12920 sample(ˇ)
12921 }
12922 "
12923 .unindent(),
12924 );
12925 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12926 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12927 .await;
12928 cx.update_editor(|editor, _, _| {
12929 let signature_help_state = editor.signature_help_state.popover().cloned();
12930 assert!(signature_help_state.is_some());
12931 let signature = signature_help_state.unwrap();
12932 assert_eq!(
12933 signature.signatures[signature.current_signature].label,
12934 "fn sample(param1: u8, param2: u8)"
12935 );
12936 editor.signature_help_state = SignatureHelpState::default();
12937 });
12938
12939 // Ensure that signature_help is called when auto signature help override is enabled
12940 cx.update(|_, cx| {
12941 cx.update_global::<SettingsStore, _>(|settings, cx| {
12942 settings.update_user_settings(cx, |settings| {
12943 settings.editor.auto_signature_help = Some(true);
12944 settings.editor.show_signature_help_after_edits = Some(false);
12945 });
12946 });
12947 });
12948 cx.set_state(
12949 &r#"
12950 fn main() {
12951 sampleˇ
12952 }
12953 "#
12954 .unindent(),
12955 );
12956 cx.update_editor(|editor, window, cx| {
12957 editor.handle_input("(", window, cx);
12958 });
12959 cx.assert_editor_state(
12960 &"
12961 fn main() {
12962 sample(ˇ)
12963 }
12964 "
12965 .unindent(),
12966 );
12967 handle_signature_help_request(&mut cx, mocked_response).await;
12968 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12969 .await;
12970 cx.editor(|editor, _, _| {
12971 let signature_help_state = editor.signature_help_state.popover().cloned();
12972 assert!(signature_help_state.is_some());
12973 let signature = signature_help_state.unwrap();
12974 assert_eq!(
12975 signature.signatures[signature.current_signature].label,
12976 "fn sample(param1: u8, param2: u8)"
12977 );
12978 });
12979}
12980
12981#[gpui::test]
12982async fn test_signature_help(cx: &mut TestAppContext) {
12983 init_test(cx, |_| {});
12984 cx.update(|cx| {
12985 cx.update_global::<SettingsStore, _>(|settings, cx| {
12986 settings.update_user_settings(cx, |settings| {
12987 settings.editor.auto_signature_help = Some(true);
12988 });
12989 });
12990 });
12991
12992 let mut cx = EditorLspTestContext::new_rust(
12993 lsp::ServerCapabilities {
12994 signature_help_provider: Some(lsp::SignatureHelpOptions {
12995 ..Default::default()
12996 }),
12997 ..Default::default()
12998 },
12999 cx,
13000 )
13001 .await;
13002
13003 // A test that directly calls `show_signature_help`
13004 cx.update_editor(|editor, window, cx| {
13005 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13006 });
13007
13008 let mocked_response = lsp::SignatureHelp {
13009 signatures: vec![lsp::SignatureInformation {
13010 label: "fn sample(param1: u8, param2: u8)".to_string(),
13011 documentation: None,
13012 parameters: Some(vec![
13013 lsp::ParameterInformation {
13014 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13015 documentation: None,
13016 },
13017 lsp::ParameterInformation {
13018 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13019 documentation: None,
13020 },
13021 ]),
13022 active_parameter: None,
13023 }],
13024 active_signature: Some(0),
13025 active_parameter: Some(0),
13026 };
13027 handle_signature_help_request(&mut cx, mocked_response).await;
13028
13029 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13030 .await;
13031
13032 cx.editor(|editor, _, _| {
13033 let signature_help_state = editor.signature_help_state.popover().cloned();
13034 assert!(signature_help_state.is_some());
13035 let signature = signature_help_state.unwrap();
13036 assert_eq!(
13037 signature.signatures[signature.current_signature].label,
13038 "fn sample(param1: u8, param2: u8)"
13039 );
13040 });
13041
13042 // When exiting outside from inside the brackets, `signature_help` is closed.
13043 cx.set_state(indoc! {"
13044 fn main() {
13045 sample(ˇ);
13046 }
13047
13048 fn sample(param1: u8, param2: u8) {}
13049 "});
13050
13051 cx.update_editor(|editor, window, cx| {
13052 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13053 s.select_ranges([0..0])
13054 });
13055 });
13056
13057 let mocked_response = lsp::SignatureHelp {
13058 signatures: Vec::new(),
13059 active_signature: None,
13060 active_parameter: None,
13061 };
13062 handle_signature_help_request(&mut cx, mocked_response).await;
13063
13064 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13065 .await;
13066
13067 cx.editor(|editor, _, _| {
13068 assert!(!editor.signature_help_state.is_shown());
13069 });
13070
13071 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13072 cx.set_state(indoc! {"
13073 fn main() {
13074 sample(ˇ);
13075 }
13076
13077 fn sample(param1: u8, param2: u8) {}
13078 "});
13079
13080 let mocked_response = lsp::SignatureHelp {
13081 signatures: vec![lsp::SignatureInformation {
13082 label: "fn sample(param1: u8, param2: u8)".to_string(),
13083 documentation: None,
13084 parameters: Some(vec![
13085 lsp::ParameterInformation {
13086 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13087 documentation: None,
13088 },
13089 lsp::ParameterInformation {
13090 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13091 documentation: None,
13092 },
13093 ]),
13094 active_parameter: None,
13095 }],
13096 active_signature: Some(0),
13097 active_parameter: Some(0),
13098 };
13099 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13100 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13101 .await;
13102 cx.editor(|editor, _, _| {
13103 assert!(editor.signature_help_state.is_shown());
13104 });
13105
13106 // Restore the popover with more parameter input
13107 cx.set_state(indoc! {"
13108 fn main() {
13109 sample(param1, param2ˇ);
13110 }
13111
13112 fn sample(param1: u8, param2: u8) {}
13113 "});
13114
13115 let mocked_response = lsp::SignatureHelp {
13116 signatures: vec![lsp::SignatureInformation {
13117 label: "fn sample(param1: u8, param2: u8)".to_string(),
13118 documentation: None,
13119 parameters: Some(vec![
13120 lsp::ParameterInformation {
13121 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13122 documentation: None,
13123 },
13124 lsp::ParameterInformation {
13125 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13126 documentation: None,
13127 },
13128 ]),
13129 active_parameter: None,
13130 }],
13131 active_signature: Some(0),
13132 active_parameter: Some(1),
13133 };
13134 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13135 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13136 .await;
13137
13138 // When selecting a range, the popover is gone.
13139 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13140 cx.update_editor(|editor, window, cx| {
13141 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13142 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13143 })
13144 });
13145 cx.assert_editor_state(indoc! {"
13146 fn main() {
13147 sample(param1, «ˇparam2»);
13148 }
13149
13150 fn sample(param1: u8, param2: u8) {}
13151 "});
13152 cx.editor(|editor, _, _| {
13153 assert!(!editor.signature_help_state.is_shown());
13154 });
13155
13156 // When unselecting again, the popover is back if within the brackets.
13157 cx.update_editor(|editor, window, cx| {
13158 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13159 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13160 })
13161 });
13162 cx.assert_editor_state(indoc! {"
13163 fn main() {
13164 sample(param1, ˇparam2);
13165 }
13166
13167 fn sample(param1: u8, param2: u8) {}
13168 "});
13169 handle_signature_help_request(&mut cx, mocked_response).await;
13170 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13171 .await;
13172 cx.editor(|editor, _, _| {
13173 assert!(editor.signature_help_state.is_shown());
13174 });
13175
13176 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13177 cx.update_editor(|editor, window, cx| {
13178 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13179 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13180 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13181 })
13182 });
13183 cx.assert_editor_state(indoc! {"
13184 fn main() {
13185 sample(param1, ˇparam2);
13186 }
13187
13188 fn sample(param1: u8, param2: u8) {}
13189 "});
13190
13191 let mocked_response = lsp::SignatureHelp {
13192 signatures: vec![lsp::SignatureInformation {
13193 label: "fn sample(param1: u8, param2: u8)".to_string(),
13194 documentation: None,
13195 parameters: Some(vec![
13196 lsp::ParameterInformation {
13197 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13198 documentation: None,
13199 },
13200 lsp::ParameterInformation {
13201 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13202 documentation: None,
13203 },
13204 ]),
13205 active_parameter: None,
13206 }],
13207 active_signature: Some(0),
13208 active_parameter: Some(1),
13209 };
13210 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13211 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13212 .await;
13213 cx.update_editor(|editor, _, cx| {
13214 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13215 });
13216 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13217 .await;
13218 cx.update_editor(|editor, window, cx| {
13219 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13220 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13221 })
13222 });
13223 cx.assert_editor_state(indoc! {"
13224 fn main() {
13225 sample(param1, «ˇparam2»);
13226 }
13227
13228 fn sample(param1: u8, param2: u8) {}
13229 "});
13230 cx.update_editor(|editor, window, cx| {
13231 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13232 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13233 })
13234 });
13235 cx.assert_editor_state(indoc! {"
13236 fn main() {
13237 sample(param1, ˇparam2);
13238 }
13239
13240 fn sample(param1: u8, param2: u8) {}
13241 "});
13242 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13243 .await;
13244}
13245
13246#[gpui::test]
13247async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13248 init_test(cx, |_| {});
13249
13250 let mut cx = EditorLspTestContext::new_rust(
13251 lsp::ServerCapabilities {
13252 signature_help_provider: Some(lsp::SignatureHelpOptions {
13253 ..Default::default()
13254 }),
13255 ..Default::default()
13256 },
13257 cx,
13258 )
13259 .await;
13260
13261 cx.set_state(indoc! {"
13262 fn main() {
13263 overloadedˇ
13264 }
13265 "});
13266
13267 cx.update_editor(|editor, window, cx| {
13268 editor.handle_input("(", window, cx);
13269 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13270 });
13271
13272 // Mock response with 3 signatures
13273 let mocked_response = lsp::SignatureHelp {
13274 signatures: vec![
13275 lsp::SignatureInformation {
13276 label: "fn overloaded(x: i32)".to_string(),
13277 documentation: None,
13278 parameters: Some(vec![lsp::ParameterInformation {
13279 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13280 documentation: None,
13281 }]),
13282 active_parameter: None,
13283 },
13284 lsp::SignatureInformation {
13285 label: "fn overloaded(x: i32, y: i32)".to_string(),
13286 documentation: None,
13287 parameters: Some(vec![
13288 lsp::ParameterInformation {
13289 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13290 documentation: None,
13291 },
13292 lsp::ParameterInformation {
13293 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13294 documentation: None,
13295 },
13296 ]),
13297 active_parameter: None,
13298 },
13299 lsp::SignatureInformation {
13300 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13301 documentation: None,
13302 parameters: Some(vec![
13303 lsp::ParameterInformation {
13304 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13305 documentation: None,
13306 },
13307 lsp::ParameterInformation {
13308 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13309 documentation: None,
13310 },
13311 lsp::ParameterInformation {
13312 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13313 documentation: None,
13314 },
13315 ]),
13316 active_parameter: None,
13317 },
13318 ],
13319 active_signature: Some(1),
13320 active_parameter: Some(0),
13321 };
13322 handle_signature_help_request(&mut cx, mocked_response).await;
13323
13324 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13325 .await;
13326
13327 // Verify we have multiple signatures and the right one is selected
13328 cx.editor(|editor, _, _| {
13329 let popover = editor.signature_help_state.popover().cloned().unwrap();
13330 assert_eq!(popover.signatures.len(), 3);
13331 // active_signature was 1, so that should be the current
13332 assert_eq!(popover.current_signature, 1);
13333 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13334 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13335 assert_eq!(
13336 popover.signatures[2].label,
13337 "fn overloaded(x: i32, y: i32, z: i32)"
13338 );
13339 });
13340
13341 // Test navigation functionality
13342 cx.update_editor(|editor, window, cx| {
13343 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13344 });
13345
13346 cx.editor(|editor, _, _| {
13347 let popover = editor.signature_help_state.popover().cloned().unwrap();
13348 assert_eq!(popover.current_signature, 2);
13349 });
13350
13351 // Test wrap around
13352 cx.update_editor(|editor, window, cx| {
13353 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13354 });
13355
13356 cx.editor(|editor, _, _| {
13357 let popover = editor.signature_help_state.popover().cloned().unwrap();
13358 assert_eq!(popover.current_signature, 0);
13359 });
13360
13361 // Test previous navigation
13362 cx.update_editor(|editor, window, cx| {
13363 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13364 });
13365
13366 cx.editor(|editor, _, _| {
13367 let popover = editor.signature_help_state.popover().cloned().unwrap();
13368 assert_eq!(popover.current_signature, 2);
13369 });
13370}
13371
13372#[gpui::test]
13373async fn test_completion_mode(cx: &mut TestAppContext) {
13374 init_test(cx, |_| {});
13375 let mut cx = EditorLspTestContext::new_rust(
13376 lsp::ServerCapabilities {
13377 completion_provider: Some(lsp::CompletionOptions {
13378 resolve_provider: Some(true),
13379 ..Default::default()
13380 }),
13381 ..Default::default()
13382 },
13383 cx,
13384 )
13385 .await;
13386
13387 struct Run {
13388 run_description: &'static str,
13389 initial_state: String,
13390 buffer_marked_text: String,
13391 completion_label: &'static str,
13392 completion_text: &'static str,
13393 expected_with_insert_mode: String,
13394 expected_with_replace_mode: String,
13395 expected_with_replace_subsequence_mode: String,
13396 expected_with_replace_suffix_mode: String,
13397 }
13398
13399 let runs = [
13400 Run {
13401 run_description: "Start of word matches completion text",
13402 initial_state: "before ediˇ after".into(),
13403 buffer_marked_text: "before <edi|> after".into(),
13404 completion_label: "editor",
13405 completion_text: "editor",
13406 expected_with_insert_mode: "before editorˇ after".into(),
13407 expected_with_replace_mode: "before editorˇ after".into(),
13408 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13409 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13410 },
13411 Run {
13412 run_description: "Accept same text at the middle of the word",
13413 initial_state: "before ediˇtor after".into(),
13414 buffer_marked_text: "before <edi|tor> after".into(),
13415 completion_label: "editor",
13416 completion_text: "editor",
13417 expected_with_insert_mode: "before editorˇtor after".into(),
13418 expected_with_replace_mode: "before editorˇ after".into(),
13419 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13420 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13421 },
13422 Run {
13423 run_description: "End of word matches completion text -- cursor at end",
13424 initial_state: "before torˇ after".into(),
13425 buffer_marked_text: "before <tor|> after".into(),
13426 completion_label: "editor",
13427 completion_text: "editor",
13428 expected_with_insert_mode: "before editorˇ after".into(),
13429 expected_with_replace_mode: "before editorˇ after".into(),
13430 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13431 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13432 },
13433 Run {
13434 run_description: "End of word matches completion text -- cursor at start",
13435 initial_state: "before ˇtor after".into(),
13436 buffer_marked_text: "before <|tor> after".into(),
13437 completion_label: "editor",
13438 completion_text: "editor",
13439 expected_with_insert_mode: "before editorˇtor after".into(),
13440 expected_with_replace_mode: "before editorˇ after".into(),
13441 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13442 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13443 },
13444 Run {
13445 run_description: "Prepend text containing whitespace",
13446 initial_state: "pˇfield: bool".into(),
13447 buffer_marked_text: "<p|field>: bool".into(),
13448 completion_label: "pub ",
13449 completion_text: "pub ",
13450 expected_with_insert_mode: "pub ˇfield: bool".into(),
13451 expected_with_replace_mode: "pub ˇ: bool".into(),
13452 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13453 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13454 },
13455 Run {
13456 run_description: "Add element to start of list",
13457 initial_state: "[element_ˇelement_2]".into(),
13458 buffer_marked_text: "[<element_|element_2>]".into(),
13459 completion_label: "element_1",
13460 completion_text: "element_1",
13461 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13462 expected_with_replace_mode: "[element_1ˇ]".into(),
13463 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13464 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13465 },
13466 Run {
13467 run_description: "Add element to start of list -- first and second elements are equal",
13468 initial_state: "[elˇelement]".into(),
13469 buffer_marked_text: "[<el|element>]".into(),
13470 completion_label: "element",
13471 completion_text: "element",
13472 expected_with_insert_mode: "[elementˇelement]".into(),
13473 expected_with_replace_mode: "[elementˇ]".into(),
13474 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13475 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13476 },
13477 Run {
13478 run_description: "Ends with matching suffix",
13479 initial_state: "SubˇError".into(),
13480 buffer_marked_text: "<Sub|Error>".into(),
13481 completion_label: "SubscriptionError",
13482 completion_text: "SubscriptionError",
13483 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13484 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13485 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13486 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13487 },
13488 Run {
13489 run_description: "Suffix is a subsequence -- contiguous",
13490 initial_state: "SubˇErr".into(),
13491 buffer_marked_text: "<Sub|Err>".into(),
13492 completion_label: "SubscriptionError",
13493 completion_text: "SubscriptionError",
13494 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13495 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13496 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13497 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13498 },
13499 Run {
13500 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13501 initial_state: "Suˇscrirr".into(),
13502 buffer_marked_text: "<Su|scrirr>".into(),
13503 completion_label: "SubscriptionError",
13504 completion_text: "SubscriptionError",
13505 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13506 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13507 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13508 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13509 },
13510 Run {
13511 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13512 initial_state: "foo(indˇix)".into(),
13513 buffer_marked_text: "foo(<ind|ix>)".into(),
13514 completion_label: "node_index",
13515 completion_text: "node_index",
13516 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13517 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13518 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13519 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13520 },
13521 Run {
13522 run_description: "Replace range ends before cursor - should extend to cursor",
13523 initial_state: "before editˇo after".into(),
13524 buffer_marked_text: "before <{ed}>it|o after".into(),
13525 completion_label: "editor",
13526 completion_text: "editor",
13527 expected_with_insert_mode: "before editorˇo after".into(),
13528 expected_with_replace_mode: "before editorˇo after".into(),
13529 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13530 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13531 },
13532 Run {
13533 run_description: "Uses label for suffix matching",
13534 initial_state: "before ediˇtor after".into(),
13535 buffer_marked_text: "before <edi|tor> after".into(),
13536 completion_label: "editor",
13537 completion_text: "editor()",
13538 expected_with_insert_mode: "before editor()ˇtor after".into(),
13539 expected_with_replace_mode: "before editor()ˇ after".into(),
13540 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13541 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13542 },
13543 Run {
13544 run_description: "Case insensitive subsequence and suffix matching",
13545 initial_state: "before EDiˇtoR after".into(),
13546 buffer_marked_text: "before <EDi|toR> after".into(),
13547 completion_label: "editor",
13548 completion_text: "editor",
13549 expected_with_insert_mode: "before editorˇtoR after".into(),
13550 expected_with_replace_mode: "before editorˇ after".into(),
13551 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13552 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13553 },
13554 ];
13555
13556 for run in runs {
13557 let run_variations = [
13558 (LspInsertMode::Insert, run.expected_with_insert_mode),
13559 (LspInsertMode::Replace, run.expected_with_replace_mode),
13560 (
13561 LspInsertMode::ReplaceSubsequence,
13562 run.expected_with_replace_subsequence_mode,
13563 ),
13564 (
13565 LspInsertMode::ReplaceSuffix,
13566 run.expected_with_replace_suffix_mode,
13567 ),
13568 ];
13569
13570 for (lsp_insert_mode, expected_text) in run_variations {
13571 eprintln!(
13572 "run = {:?}, mode = {lsp_insert_mode:.?}",
13573 run.run_description,
13574 );
13575
13576 update_test_language_settings(&mut cx, |settings| {
13577 settings.defaults.completions = Some(CompletionSettingsContent {
13578 lsp_insert_mode: Some(lsp_insert_mode),
13579 words: Some(WordsCompletionMode::Disabled),
13580 words_min_length: Some(0),
13581 ..Default::default()
13582 });
13583 });
13584
13585 cx.set_state(&run.initial_state);
13586 cx.update_editor(|editor, window, cx| {
13587 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13588 });
13589
13590 let counter = Arc::new(AtomicUsize::new(0));
13591 handle_completion_request_with_insert_and_replace(
13592 &mut cx,
13593 &run.buffer_marked_text,
13594 vec![(run.completion_label, run.completion_text)],
13595 counter.clone(),
13596 )
13597 .await;
13598 cx.condition(|editor, _| editor.context_menu_visible())
13599 .await;
13600 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13601
13602 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13603 editor
13604 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13605 .unwrap()
13606 });
13607 cx.assert_editor_state(&expected_text);
13608 handle_resolve_completion_request(&mut cx, None).await;
13609 apply_additional_edits.await.unwrap();
13610 }
13611 }
13612}
13613
13614#[gpui::test]
13615async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13616 init_test(cx, |_| {});
13617 let mut cx = EditorLspTestContext::new_rust(
13618 lsp::ServerCapabilities {
13619 completion_provider: Some(lsp::CompletionOptions {
13620 resolve_provider: Some(true),
13621 ..Default::default()
13622 }),
13623 ..Default::default()
13624 },
13625 cx,
13626 )
13627 .await;
13628
13629 let initial_state = "SubˇError";
13630 let buffer_marked_text = "<Sub|Error>";
13631 let completion_text = "SubscriptionError";
13632 let expected_with_insert_mode = "SubscriptionErrorˇError";
13633 let expected_with_replace_mode = "SubscriptionErrorˇ";
13634
13635 update_test_language_settings(&mut cx, |settings| {
13636 settings.defaults.completions = Some(CompletionSettingsContent {
13637 words: Some(WordsCompletionMode::Disabled),
13638 words_min_length: Some(0),
13639 // set the opposite here to ensure that the action is overriding the default behavior
13640 lsp_insert_mode: Some(LspInsertMode::Insert),
13641 ..Default::default()
13642 });
13643 });
13644
13645 cx.set_state(initial_state);
13646 cx.update_editor(|editor, window, cx| {
13647 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13648 });
13649
13650 let counter = Arc::new(AtomicUsize::new(0));
13651 handle_completion_request_with_insert_and_replace(
13652 &mut cx,
13653 buffer_marked_text,
13654 vec![(completion_text, completion_text)],
13655 counter.clone(),
13656 )
13657 .await;
13658 cx.condition(|editor, _| editor.context_menu_visible())
13659 .await;
13660 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13661
13662 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13663 editor
13664 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13665 .unwrap()
13666 });
13667 cx.assert_editor_state(expected_with_replace_mode);
13668 handle_resolve_completion_request(&mut cx, None).await;
13669 apply_additional_edits.await.unwrap();
13670
13671 update_test_language_settings(&mut cx, |settings| {
13672 settings.defaults.completions = Some(CompletionSettingsContent {
13673 words: Some(WordsCompletionMode::Disabled),
13674 words_min_length: Some(0),
13675 // set the opposite here to ensure that the action is overriding the default behavior
13676 lsp_insert_mode: Some(LspInsertMode::Replace),
13677 ..Default::default()
13678 });
13679 });
13680
13681 cx.set_state(initial_state);
13682 cx.update_editor(|editor, window, cx| {
13683 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13684 });
13685 handle_completion_request_with_insert_and_replace(
13686 &mut cx,
13687 buffer_marked_text,
13688 vec![(completion_text, completion_text)],
13689 counter.clone(),
13690 )
13691 .await;
13692 cx.condition(|editor, _| editor.context_menu_visible())
13693 .await;
13694 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13695
13696 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13697 editor
13698 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13699 .unwrap()
13700 });
13701 cx.assert_editor_state(expected_with_insert_mode);
13702 handle_resolve_completion_request(&mut cx, None).await;
13703 apply_additional_edits.await.unwrap();
13704}
13705
13706#[gpui::test]
13707async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13708 init_test(cx, |_| {});
13709 let mut cx = EditorLspTestContext::new_rust(
13710 lsp::ServerCapabilities {
13711 completion_provider: Some(lsp::CompletionOptions {
13712 resolve_provider: Some(true),
13713 ..Default::default()
13714 }),
13715 ..Default::default()
13716 },
13717 cx,
13718 )
13719 .await;
13720
13721 // scenario: surrounding text matches completion text
13722 let completion_text = "to_offset";
13723 let initial_state = indoc! {"
13724 1. buf.to_offˇsuffix
13725 2. buf.to_offˇsuf
13726 3. buf.to_offˇfix
13727 4. buf.to_offˇ
13728 5. into_offˇensive
13729 6. ˇsuffix
13730 7. let ˇ //
13731 8. aaˇzz
13732 9. buf.to_off«zzzzzˇ»suffix
13733 10. buf.«ˇzzzzz»suffix
13734 11. to_off«ˇzzzzz»
13735
13736 buf.to_offˇsuffix // newest cursor
13737 "};
13738 let completion_marked_buffer = indoc! {"
13739 1. buf.to_offsuffix
13740 2. buf.to_offsuf
13741 3. buf.to_offfix
13742 4. buf.to_off
13743 5. into_offensive
13744 6. suffix
13745 7. let //
13746 8. aazz
13747 9. buf.to_offzzzzzsuffix
13748 10. buf.zzzzzsuffix
13749 11. to_offzzzzz
13750
13751 buf.<to_off|suffix> // newest cursor
13752 "};
13753 let expected = indoc! {"
13754 1. buf.to_offsetˇ
13755 2. buf.to_offsetˇsuf
13756 3. buf.to_offsetˇfix
13757 4. buf.to_offsetˇ
13758 5. into_offsetˇensive
13759 6. to_offsetˇsuffix
13760 7. let to_offsetˇ //
13761 8. aato_offsetˇzz
13762 9. buf.to_offsetˇ
13763 10. buf.to_offsetˇsuffix
13764 11. to_offsetˇ
13765
13766 buf.to_offsetˇ // newest cursor
13767 "};
13768 cx.set_state(initial_state);
13769 cx.update_editor(|editor, window, cx| {
13770 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13771 });
13772 handle_completion_request_with_insert_and_replace(
13773 &mut cx,
13774 completion_marked_buffer,
13775 vec![(completion_text, completion_text)],
13776 Arc::new(AtomicUsize::new(0)),
13777 )
13778 .await;
13779 cx.condition(|editor, _| editor.context_menu_visible())
13780 .await;
13781 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13782 editor
13783 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13784 .unwrap()
13785 });
13786 cx.assert_editor_state(expected);
13787 handle_resolve_completion_request(&mut cx, None).await;
13788 apply_additional_edits.await.unwrap();
13789
13790 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13791 let completion_text = "foo_and_bar";
13792 let initial_state = indoc! {"
13793 1. ooanbˇ
13794 2. zooanbˇ
13795 3. ooanbˇz
13796 4. zooanbˇz
13797 5. ooanˇ
13798 6. oanbˇ
13799
13800 ooanbˇ
13801 "};
13802 let completion_marked_buffer = indoc! {"
13803 1. ooanb
13804 2. zooanb
13805 3. ooanbz
13806 4. zooanbz
13807 5. ooan
13808 6. oanb
13809
13810 <ooanb|>
13811 "};
13812 let expected = indoc! {"
13813 1. foo_and_barˇ
13814 2. zfoo_and_barˇ
13815 3. foo_and_barˇz
13816 4. zfoo_and_barˇz
13817 5. ooanfoo_and_barˇ
13818 6. oanbfoo_and_barˇ
13819
13820 foo_and_barˇ
13821 "};
13822 cx.set_state(initial_state);
13823 cx.update_editor(|editor, window, cx| {
13824 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13825 });
13826 handle_completion_request_with_insert_and_replace(
13827 &mut cx,
13828 completion_marked_buffer,
13829 vec![(completion_text, completion_text)],
13830 Arc::new(AtomicUsize::new(0)),
13831 )
13832 .await;
13833 cx.condition(|editor, _| editor.context_menu_visible())
13834 .await;
13835 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13836 editor
13837 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13838 .unwrap()
13839 });
13840 cx.assert_editor_state(expected);
13841 handle_resolve_completion_request(&mut cx, None).await;
13842 apply_additional_edits.await.unwrap();
13843
13844 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13845 // (expects the same as if it was inserted at the end)
13846 let completion_text = "foo_and_bar";
13847 let initial_state = indoc! {"
13848 1. ooˇanb
13849 2. zooˇanb
13850 3. ooˇanbz
13851 4. zooˇanbz
13852
13853 ooˇanb
13854 "};
13855 let completion_marked_buffer = indoc! {"
13856 1. ooanb
13857 2. zooanb
13858 3. ooanbz
13859 4. zooanbz
13860
13861 <oo|anb>
13862 "};
13863 let expected = indoc! {"
13864 1. foo_and_barˇ
13865 2. zfoo_and_barˇ
13866 3. foo_and_barˇz
13867 4. zfoo_and_barˇz
13868
13869 foo_and_barˇ
13870 "};
13871 cx.set_state(initial_state);
13872 cx.update_editor(|editor, window, cx| {
13873 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13874 });
13875 handle_completion_request_with_insert_and_replace(
13876 &mut cx,
13877 completion_marked_buffer,
13878 vec![(completion_text, completion_text)],
13879 Arc::new(AtomicUsize::new(0)),
13880 )
13881 .await;
13882 cx.condition(|editor, _| editor.context_menu_visible())
13883 .await;
13884 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13885 editor
13886 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13887 .unwrap()
13888 });
13889 cx.assert_editor_state(expected);
13890 handle_resolve_completion_request(&mut cx, None).await;
13891 apply_additional_edits.await.unwrap();
13892}
13893
13894// This used to crash
13895#[gpui::test]
13896async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13897 init_test(cx, |_| {});
13898
13899 let buffer_text = indoc! {"
13900 fn main() {
13901 10.satu;
13902
13903 //
13904 // separate cursors so they open in different excerpts (manually reproducible)
13905 //
13906
13907 10.satu20;
13908 }
13909 "};
13910 let multibuffer_text_with_selections = indoc! {"
13911 fn main() {
13912 10.satuˇ;
13913
13914 //
13915
13916 //
13917
13918 10.satuˇ20;
13919 }
13920 "};
13921 let expected_multibuffer = indoc! {"
13922 fn main() {
13923 10.saturating_sub()ˇ;
13924
13925 //
13926
13927 //
13928
13929 10.saturating_sub()ˇ;
13930 }
13931 "};
13932
13933 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13934 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13935
13936 let fs = FakeFs::new(cx.executor());
13937 fs.insert_tree(
13938 path!("/a"),
13939 json!({
13940 "main.rs": buffer_text,
13941 }),
13942 )
13943 .await;
13944
13945 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13946 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13947 language_registry.add(rust_lang());
13948 let mut fake_servers = language_registry.register_fake_lsp(
13949 "Rust",
13950 FakeLspAdapter {
13951 capabilities: lsp::ServerCapabilities {
13952 completion_provider: Some(lsp::CompletionOptions {
13953 resolve_provider: None,
13954 ..lsp::CompletionOptions::default()
13955 }),
13956 ..lsp::ServerCapabilities::default()
13957 },
13958 ..FakeLspAdapter::default()
13959 },
13960 );
13961 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13962 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13963 let buffer = project
13964 .update(cx, |project, cx| {
13965 project.open_local_buffer(path!("/a/main.rs"), cx)
13966 })
13967 .await
13968 .unwrap();
13969
13970 let multi_buffer = cx.new(|cx| {
13971 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13972 multi_buffer.push_excerpts(
13973 buffer.clone(),
13974 [ExcerptRange::new(0..first_excerpt_end)],
13975 cx,
13976 );
13977 multi_buffer.push_excerpts(
13978 buffer.clone(),
13979 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13980 cx,
13981 );
13982 multi_buffer
13983 });
13984
13985 let editor = workspace
13986 .update(cx, |_, window, cx| {
13987 cx.new(|cx| {
13988 Editor::new(
13989 EditorMode::Full {
13990 scale_ui_elements_with_buffer_font_size: false,
13991 show_active_line_background: false,
13992 sized_by_content: false,
13993 },
13994 multi_buffer.clone(),
13995 Some(project.clone()),
13996 window,
13997 cx,
13998 )
13999 })
14000 })
14001 .unwrap();
14002
14003 let pane = workspace
14004 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14005 .unwrap();
14006 pane.update_in(cx, |pane, window, cx| {
14007 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14008 });
14009
14010 let fake_server = fake_servers.next().await.unwrap();
14011
14012 editor.update_in(cx, |editor, window, cx| {
14013 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14014 s.select_ranges([
14015 Point::new(1, 11)..Point::new(1, 11),
14016 Point::new(7, 11)..Point::new(7, 11),
14017 ])
14018 });
14019
14020 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14021 });
14022
14023 editor.update_in(cx, |editor, window, cx| {
14024 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14025 });
14026
14027 fake_server
14028 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14029 let completion_item = lsp::CompletionItem {
14030 label: "saturating_sub()".into(),
14031 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14032 lsp::InsertReplaceEdit {
14033 new_text: "saturating_sub()".to_owned(),
14034 insert: lsp::Range::new(
14035 lsp::Position::new(7, 7),
14036 lsp::Position::new(7, 11),
14037 ),
14038 replace: lsp::Range::new(
14039 lsp::Position::new(7, 7),
14040 lsp::Position::new(7, 13),
14041 ),
14042 },
14043 )),
14044 ..lsp::CompletionItem::default()
14045 };
14046
14047 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14048 })
14049 .next()
14050 .await
14051 .unwrap();
14052
14053 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14054 .await;
14055
14056 editor
14057 .update_in(cx, |editor, window, cx| {
14058 editor
14059 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14060 .unwrap()
14061 })
14062 .await
14063 .unwrap();
14064
14065 editor.update(cx, |editor, cx| {
14066 assert_text_with_selections(editor, expected_multibuffer, cx);
14067 })
14068}
14069
14070#[gpui::test]
14071async fn test_completion(cx: &mut TestAppContext) {
14072 init_test(cx, |_| {});
14073
14074 let mut cx = EditorLspTestContext::new_rust(
14075 lsp::ServerCapabilities {
14076 completion_provider: Some(lsp::CompletionOptions {
14077 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14078 resolve_provider: Some(true),
14079 ..Default::default()
14080 }),
14081 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14082 ..Default::default()
14083 },
14084 cx,
14085 )
14086 .await;
14087 let counter = Arc::new(AtomicUsize::new(0));
14088
14089 cx.set_state(indoc! {"
14090 oneˇ
14091 two
14092 three
14093 "});
14094 cx.simulate_keystroke(".");
14095 handle_completion_request(
14096 indoc! {"
14097 one.|<>
14098 two
14099 three
14100 "},
14101 vec!["first_completion", "second_completion"],
14102 true,
14103 counter.clone(),
14104 &mut cx,
14105 )
14106 .await;
14107 cx.condition(|editor, _| editor.context_menu_visible())
14108 .await;
14109 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14110
14111 let _handler = handle_signature_help_request(
14112 &mut cx,
14113 lsp::SignatureHelp {
14114 signatures: vec![lsp::SignatureInformation {
14115 label: "test signature".to_string(),
14116 documentation: None,
14117 parameters: Some(vec![lsp::ParameterInformation {
14118 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14119 documentation: None,
14120 }]),
14121 active_parameter: None,
14122 }],
14123 active_signature: None,
14124 active_parameter: None,
14125 },
14126 );
14127 cx.update_editor(|editor, window, cx| {
14128 assert!(
14129 !editor.signature_help_state.is_shown(),
14130 "No signature help was called for"
14131 );
14132 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14133 });
14134 cx.run_until_parked();
14135 cx.update_editor(|editor, _, _| {
14136 assert!(
14137 !editor.signature_help_state.is_shown(),
14138 "No signature help should be shown when completions menu is open"
14139 );
14140 });
14141
14142 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14143 editor.context_menu_next(&Default::default(), window, cx);
14144 editor
14145 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14146 .unwrap()
14147 });
14148 cx.assert_editor_state(indoc! {"
14149 one.second_completionˇ
14150 two
14151 three
14152 "});
14153
14154 handle_resolve_completion_request(
14155 &mut cx,
14156 Some(vec![
14157 (
14158 //This overlaps with the primary completion edit which is
14159 //misbehavior from the LSP spec, test that we filter it out
14160 indoc! {"
14161 one.second_ˇcompletion
14162 two
14163 threeˇ
14164 "},
14165 "overlapping additional edit",
14166 ),
14167 (
14168 indoc! {"
14169 one.second_completion
14170 two
14171 threeˇ
14172 "},
14173 "\nadditional edit",
14174 ),
14175 ]),
14176 )
14177 .await;
14178 apply_additional_edits.await.unwrap();
14179 cx.assert_editor_state(indoc! {"
14180 one.second_completionˇ
14181 two
14182 three
14183 additional edit
14184 "});
14185
14186 cx.set_state(indoc! {"
14187 one.second_completion
14188 twoˇ
14189 threeˇ
14190 additional edit
14191 "});
14192 cx.simulate_keystroke(" ");
14193 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14194 cx.simulate_keystroke("s");
14195 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14196
14197 cx.assert_editor_state(indoc! {"
14198 one.second_completion
14199 two sˇ
14200 three sˇ
14201 additional edit
14202 "});
14203 handle_completion_request(
14204 indoc! {"
14205 one.second_completion
14206 two s
14207 three <s|>
14208 additional edit
14209 "},
14210 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14211 true,
14212 counter.clone(),
14213 &mut cx,
14214 )
14215 .await;
14216 cx.condition(|editor, _| editor.context_menu_visible())
14217 .await;
14218 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14219
14220 cx.simulate_keystroke("i");
14221
14222 handle_completion_request(
14223 indoc! {"
14224 one.second_completion
14225 two si
14226 three <si|>
14227 additional edit
14228 "},
14229 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14230 true,
14231 counter.clone(),
14232 &mut cx,
14233 )
14234 .await;
14235 cx.condition(|editor, _| editor.context_menu_visible())
14236 .await;
14237 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14238
14239 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14240 editor
14241 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14242 .unwrap()
14243 });
14244 cx.assert_editor_state(indoc! {"
14245 one.second_completion
14246 two sixth_completionˇ
14247 three sixth_completionˇ
14248 additional edit
14249 "});
14250
14251 apply_additional_edits.await.unwrap();
14252
14253 update_test_language_settings(&mut cx, |settings| {
14254 settings.defaults.show_completions_on_input = Some(false);
14255 });
14256 cx.set_state("editorˇ");
14257 cx.simulate_keystroke(".");
14258 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14259 cx.simulate_keystrokes("c l o");
14260 cx.assert_editor_state("editor.cloˇ");
14261 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14262 cx.update_editor(|editor, window, cx| {
14263 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14264 });
14265 handle_completion_request(
14266 "editor.<clo|>",
14267 vec!["close", "clobber"],
14268 true,
14269 counter.clone(),
14270 &mut cx,
14271 )
14272 .await;
14273 cx.condition(|editor, _| editor.context_menu_visible())
14274 .await;
14275 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14276
14277 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14278 editor
14279 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14280 .unwrap()
14281 });
14282 cx.assert_editor_state("editor.clobberˇ");
14283 handle_resolve_completion_request(&mut cx, None).await;
14284 apply_additional_edits.await.unwrap();
14285}
14286
14287#[gpui::test]
14288async fn test_completion_reuse(cx: &mut TestAppContext) {
14289 init_test(cx, |_| {});
14290
14291 let mut cx = EditorLspTestContext::new_rust(
14292 lsp::ServerCapabilities {
14293 completion_provider: Some(lsp::CompletionOptions {
14294 trigger_characters: Some(vec![".".to_string()]),
14295 ..Default::default()
14296 }),
14297 ..Default::default()
14298 },
14299 cx,
14300 )
14301 .await;
14302
14303 let counter = Arc::new(AtomicUsize::new(0));
14304 cx.set_state("objˇ");
14305 cx.simulate_keystroke(".");
14306
14307 // Initial completion request returns complete results
14308 let is_incomplete = false;
14309 handle_completion_request(
14310 "obj.|<>",
14311 vec!["a", "ab", "abc"],
14312 is_incomplete,
14313 counter.clone(),
14314 &mut cx,
14315 )
14316 .await;
14317 cx.run_until_parked();
14318 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14319 cx.assert_editor_state("obj.ˇ");
14320 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14321
14322 // Type "a" - filters existing completions
14323 cx.simulate_keystroke("a");
14324 cx.run_until_parked();
14325 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14326 cx.assert_editor_state("obj.aˇ");
14327 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14328
14329 // Type "b" - filters existing completions
14330 cx.simulate_keystroke("b");
14331 cx.run_until_parked();
14332 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14333 cx.assert_editor_state("obj.abˇ");
14334 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14335
14336 // Type "c" - filters existing completions
14337 cx.simulate_keystroke("c");
14338 cx.run_until_parked();
14339 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14340 cx.assert_editor_state("obj.abcˇ");
14341 check_displayed_completions(vec!["abc"], &mut cx);
14342
14343 // Backspace to delete "c" - filters existing completions
14344 cx.update_editor(|editor, window, cx| {
14345 editor.backspace(&Backspace, window, cx);
14346 });
14347 cx.run_until_parked();
14348 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14349 cx.assert_editor_state("obj.abˇ");
14350 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14351
14352 // Moving cursor to the left dismisses menu.
14353 cx.update_editor(|editor, window, cx| {
14354 editor.move_left(&MoveLeft, window, cx);
14355 });
14356 cx.run_until_parked();
14357 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14358 cx.assert_editor_state("obj.aˇb");
14359 cx.update_editor(|editor, _, _| {
14360 assert_eq!(editor.context_menu_visible(), false);
14361 });
14362
14363 // Type "b" - new request
14364 cx.simulate_keystroke("b");
14365 let is_incomplete = false;
14366 handle_completion_request(
14367 "obj.<ab|>a",
14368 vec!["ab", "abc"],
14369 is_incomplete,
14370 counter.clone(),
14371 &mut cx,
14372 )
14373 .await;
14374 cx.run_until_parked();
14375 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14376 cx.assert_editor_state("obj.abˇb");
14377 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14378
14379 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14380 cx.update_editor(|editor, window, cx| {
14381 editor.backspace(&Backspace, window, cx);
14382 });
14383 let is_incomplete = false;
14384 handle_completion_request(
14385 "obj.<a|>b",
14386 vec!["a", "ab", "abc"],
14387 is_incomplete,
14388 counter.clone(),
14389 &mut cx,
14390 )
14391 .await;
14392 cx.run_until_parked();
14393 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14394 cx.assert_editor_state("obj.aˇb");
14395 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14396
14397 // Backspace to delete "a" - dismisses menu.
14398 cx.update_editor(|editor, window, cx| {
14399 editor.backspace(&Backspace, window, cx);
14400 });
14401 cx.run_until_parked();
14402 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14403 cx.assert_editor_state("obj.ˇb");
14404 cx.update_editor(|editor, _, _| {
14405 assert_eq!(editor.context_menu_visible(), false);
14406 });
14407}
14408
14409#[gpui::test]
14410async fn test_word_completion(cx: &mut TestAppContext) {
14411 let lsp_fetch_timeout_ms = 10;
14412 init_test(cx, |language_settings| {
14413 language_settings.defaults.completions = Some(CompletionSettingsContent {
14414 words_min_length: Some(0),
14415 lsp_fetch_timeout_ms: Some(10),
14416 lsp_insert_mode: Some(LspInsertMode::Insert),
14417 ..Default::default()
14418 });
14419 });
14420
14421 let mut cx = EditorLspTestContext::new_rust(
14422 lsp::ServerCapabilities {
14423 completion_provider: Some(lsp::CompletionOptions {
14424 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14425 ..lsp::CompletionOptions::default()
14426 }),
14427 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14428 ..lsp::ServerCapabilities::default()
14429 },
14430 cx,
14431 )
14432 .await;
14433
14434 let throttle_completions = Arc::new(AtomicBool::new(false));
14435
14436 let lsp_throttle_completions = throttle_completions.clone();
14437 let _completion_requests_handler =
14438 cx.lsp
14439 .server
14440 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14441 let lsp_throttle_completions = lsp_throttle_completions.clone();
14442 let cx = cx.clone();
14443 async move {
14444 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14445 cx.background_executor()
14446 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14447 .await;
14448 }
14449 Ok(Some(lsp::CompletionResponse::Array(vec![
14450 lsp::CompletionItem {
14451 label: "first".into(),
14452 ..lsp::CompletionItem::default()
14453 },
14454 lsp::CompletionItem {
14455 label: "last".into(),
14456 ..lsp::CompletionItem::default()
14457 },
14458 ])))
14459 }
14460 });
14461
14462 cx.set_state(indoc! {"
14463 oneˇ
14464 two
14465 three
14466 "});
14467 cx.simulate_keystroke(".");
14468 cx.executor().run_until_parked();
14469 cx.condition(|editor, _| editor.context_menu_visible())
14470 .await;
14471 cx.update_editor(|editor, window, cx| {
14472 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14473 {
14474 assert_eq!(
14475 completion_menu_entries(menu),
14476 &["first", "last"],
14477 "When LSP server is fast to reply, no fallback word completions are used"
14478 );
14479 } else {
14480 panic!("expected completion menu to be open");
14481 }
14482 editor.cancel(&Cancel, window, cx);
14483 });
14484 cx.executor().run_until_parked();
14485 cx.condition(|editor, _| !editor.context_menu_visible())
14486 .await;
14487
14488 throttle_completions.store(true, atomic::Ordering::Release);
14489 cx.simulate_keystroke(".");
14490 cx.executor()
14491 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14492 cx.executor().run_until_parked();
14493 cx.condition(|editor, _| editor.context_menu_visible())
14494 .await;
14495 cx.update_editor(|editor, _, _| {
14496 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14497 {
14498 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14499 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14500 } else {
14501 panic!("expected completion menu to be open");
14502 }
14503 });
14504}
14505
14506#[gpui::test]
14507async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14508 init_test(cx, |language_settings| {
14509 language_settings.defaults.completions = Some(CompletionSettingsContent {
14510 words: Some(WordsCompletionMode::Enabled),
14511 words_min_length: Some(0),
14512 lsp_insert_mode: Some(LspInsertMode::Insert),
14513 ..Default::default()
14514 });
14515 });
14516
14517 let mut cx = EditorLspTestContext::new_rust(
14518 lsp::ServerCapabilities {
14519 completion_provider: Some(lsp::CompletionOptions {
14520 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14521 ..lsp::CompletionOptions::default()
14522 }),
14523 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14524 ..lsp::ServerCapabilities::default()
14525 },
14526 cx,
14527 )
14528 .await;
14529
14530 let _completion_requests_handler =
14531 cx.lsp
14532 .server
14533 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14534 Ok(Some(lsp::CompletionResponse::Array(vec![
14535 lsp::CompletionItem {
14536 label: "first".into(),
14537 ..lsp::CompletionItem::default()
14538 },
14539 lsp::CompletionItem {
14540 label: "last".into(),
14541 ..lsp::CompletionItem::default()
14542 },
14543 ])))
14544 });
14545
14546 cx.set_state(indoc! {"ˇ
14547 first
14548 last
14549 second
14550 "});
14551 cx.simulate_keystroke(".");
14552 cx.executor().run_until_parked();
14553 cx.condition(|editor, _| editor.context_menu_visible())
14554 .await;
14555 cx.update_editor(|editor, _, _| {
14556 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14557 {
14558 assert_eq!(
14559 completion_menu_entries(menu),
14560 &["first", "last", "second"],
14561 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14562 );
14563 } else {
14564 panic!("expected completion menu to be open");
14565 }
14566 });
14567}
14568
14569#[gpui::test]
14570async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14571 init_test(cx, |language_settings| {
14572 language_settings.defaults.completions = Some(CompletionSettingsContent {
14573 words: Some(WordsCompletionMode::Disabled),
14574 words_min_length: Some(0),
14575 lsp_insert_mode: Some(LspInsertMode::Insert),
14576 ..Default::default()
14577 });
14578 });
14579
14580 let mut cx = EditorLspTestContext::new_rust(
14581 lsp::ServerCapabilities {
14582 completion_provider: Some(lsp::CompletionOptions {
14583 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14584 ..lsp::CompletionOptions::default()
14585 }),
14586 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14587 ..lsp::ServerCapabilities::default()
14588 },
14589 cx,
14590 )
14591 .await;
14592
14593 let _completion_requests_handler =
14594 cx.lsp
14595 .server
14596 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14597 panic!("LSP completions should not be queried when dealing with word completions")
14598 });
14599
14600 cx.set_state(indoc! {"ˇ
14601 first
14602 last
14603 second
14604 "});
14605 cx.update_editor(|editor, window, cx| {
14606 editor.show_word_completions(&ShowWordCompletions, window, cx);
14607 });
14608 cx.executor().run_until_parked();
14609 cx.condition(|editor, _| editor.context_menu_visible())
14610 .await;
14611 cx.update_editor(|editor, _, _| {
14612 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14613 {
14614 assert_eq!(
14615 completion_menu_entries(menu),
14616 &["first", "last", "second"],
14617 "`ShowWordCompletions` action should show word completions"
14618 );
14619 } else {
14620 panic!("expected completion menu to be open");
14621 }
14622 });
14623
14624 cx.simulate_keystroke("l");
14625 cx.executor().run_until_parked();
14626 cx.condition(|editor, _| editor.context_menu_visible())
14627 .await;
14628 cx.update_editor(|editor, _, _| {
14629 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14630 {
14631 assert_eq!(
14632 completion_menu_entries(menu),
14633 &["last"],
14634 "After showing word completions, further editing should filter them and not query the LSP"
14635 );
14636 } else {
14637 panic!("expected completion menu to be open");
14638 }
14639 });
14640}
14641
14642#[gpui::test]
14643async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14644 init_test(cx, |language_settings| {
14645 language_settings.defaults.completions = Some(CompletionSettingsContent {
14646 words_min_length: Some(0),
14647 lsp: Some(false),
14648 lsp_insert_mode: Some(LspInsertMode::Insert),
14649 ..Default::default()
14650 });
14651 });
14652
14653 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14654
14655 cx.set_state(indoc! {"ˇ
14656 0_usize
14657 let
14658 33
14659 4.5f32
14660 "});
14661 cx.update_editor(|editor, window, cx| {
14662 editor.show_completions(&ShowCompletions::default(), window, cx);
14663 });
14664 cx.executor().run_until_parked();
14665 cx.condition(|editor, _| editor.context_menu_visible())
14666 .await;
14667 cx.update_editor(|editor, window, cx| {
14668 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14669 {
14670 assert_eq!(
14671 completion_menu_entries(menu),
14672 &["let"],
14673 "With no digits in the completion query, no digits should be in the word completions"
14674 );
14675 } else {
14676 panic!("expected completion menu to be open");
14677 }
14678 editor.cancel(&Cancel, window, cx);
14679 });
14680
14681 cx.set_state(indoc! {"3ˇ
14682 0_usize
14683 let
14684 3
14685 33.35f32
14686 "});
14687 cx.update_editor(|editor, window, cx| {
14688 editor.show_completions(&ShowCompletions::default(), window, cx);
14689 });
14690 cx.executor().run_until_parked();
14691 cx.condition(|editor, _| editor.context_menu_visible())
14692 .await;
14693 cx.update_editor(|editor, _, _| {
14694 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14695 {
14696 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14697 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14698 } else {
14699 panic!("expected completion menu to be open");
14700 }
14701 });
14702}
14703
14704#[gpui::test]
14705async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14706 init_test(cx, |language_settings| {
14707 language_settings.defaults.completions = Some(CompletionSettingsContent {
14708 words: Some(WordsCompletionMode::Enabled),
14709 words_min_length: Some(3),
14710 lsp_insert_mode: Some(LspInsertMode::Insert),
14711 ..Default::default()
14712 });
14713 });
14714
14715 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14716 cx.set_state(indoc! {"ˇ
14717 wow
14718 wowen
14719 wowser
14720 "});
14721 cx.simulate_keystroke("w");
14722 cx.executor().run_until_parked();
14723 cx.update_editor(|editor, _, _| {
14724 if editor.context_menu.borrow_mut().is_some() {
14725 panic!(
14726 "expected completion menu to be hidden, as words completion threshold is not met"
14727 );
14728 }
14729 });
14730
14731 cx.update_editor(|editor, window, cx| {
14732 editor.show_word_completions(&ShowWordCompletions, window, cx);
14733 });
14734 cx.executor().run_until_parked();
14735 cx.update_editor(|editor, window, cx| {
14736 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14737 {
14738 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");
14739 } else {
14740 panic!("expected completion menu to be open after the word completions are called with an action");
14741 }
14742
14743 editor.cancel(&Cancel, window, cx);
14744 });
14745 cx.update_editor(|editor, _, _| {
14746 if editor.context_menu.borrow_mut().is_some() {
14747 panic!("expected completion menu to be hidden after canceling");
14748 }
14749 });
14750
14751 cx.simulate_keystroke("o");
14752 cx.executor().run_until_parked();
14753 cx.update_editor(|editor, _, _| {
14754 if editor.context_menu.borrow_mut().is_some() {
14755 panic!(
14756 "expected completion menu to be hidden, as words completion threshold is not met still"
14757 );
14758 }
14759 });
14760
14761 cx.simulate_keystroke("w");
14762 cx.executor().run_until_parked();
14763 cx.update_editor(|editor, _, _| {
14764 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14765 {
14766 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14767 } else {
14768 panic!("expected completion menu to be open after the word completions threshold is met");
14769 }
14770 });
14771}
14772
14773#[gpui::test]
14774async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14775 init_test(cx, |language_settings| {
14776 language_settings.defaults.completions = Some(CompletionSettingsContent {
14777 words: Some(WordsCompletionMode::Enabled),
14778 words_min_length: Some(0),
14779 lsp_insert_mode: Some(LspInsertMode::Insert),
14780 ..Default::default()
14781 });
14782 });
14783
14784 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14785 cx.update_editor(|editor, _, _| {
14786 editor.disable_word_completions();
14787 });
14788 cx.set_state(indoc! {"ˇ
14789 wow
14790 wowen
14791 wowser
14792 "});
14793 cx.simulate_keystroke("w");
14794 cx.executor().run_until_parked();
14795 cx.update_editor(|editor, _, _| {
14796 if editor.context_menu.borrow_mut().is_some() {
14797 panic!(
14798 "expected completion menu to be hidden, as words completion are disabled for this editor"
14799 );
14800 }
14801 });
14802
14803 cx.update_editor(|editor, window, cx| {
14804 editor.show_word_completions(&ShowWordCompletions, window, cx);
14805 });
14806 cx.executor().run_until_parked();
14807 cx.update_editor(|editor, _, _| {
14808 if editor.context_menu.borrow_mut().is_some() {
14809 panic!(
14810 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14811 );
14812 }
14813 });
14814}
14815
14816fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14817 let position = || lsp::Position {
14818 line: params.text_document_position.position.line,
14819 character: params.text_document_position.position.character,
14820 };
14821 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14822 range: lsp::Range {
14823 start: position(),
14824 end: position(),
14825 },
14826 new_text: text.to_string(),
14827 }))
14828}
14829
14830#[gpui::test]
14831async fn test_multiline_completion(cx: &mut TestAppContext) {
14832 init_test(cx, |_| {});
14833
14834 let fs = FakeFs::new(cx.executor());
14835 fs.insert_tree(
14836 path!("/a"),
14837 json!({
14838 "main.ts": "a",
14839 }),
14840 )
14841 .await;
14842
14843 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14844 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14845 let typescript_language = Arc::new(Language::new(
14846 LanguageConfig {
14847 name: "TypeScript".into(),
14848 matcher: LanguageMatcher {
14849 path_suffixes: vec!["ts".to_string()],
14850 ..LanguageMatcher::default()
14851 },
14852 line_comments: vec!["// ".into()],
14853 ..LanguageConfig::default()
14854 },
14855 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14856 ));
14857 language_registry.add(typescript_language.clone());
14858 let mut fake_servers = language_registry.register_fake_lsp(
14859 "TypeScript",
14860 FakeLspAdapter {
14861 capabilities: lsp::ServerCapabilities {
14862 completion_provider: Some(lsp::CompletionOptions {
14863 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14864 ..lsp::CompletionOptions::default()
14865 }),
14866 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14867 ..lsp::ServerCapabilities::default()
14868 },
14869 // Emulate vtsls label generation
14870 label_for_completion: Some(Box::new(|item, _| {
14871 let text = if let Some(description) = item
14872 .label_details
14873 .as_ref()
14874 .and_then(|label_details| label_details.description.as_ref())
14875 {
14876 format!("{} {}", item.label, description)
14877 } else if let Some(detail) = &item.detail {
14878 format!("{} {}", item.label, detail)
14879 } else {
14880 item.label.clone()
14881 };
14882 let len = text.len();
14883 Some(language::CodeLabel {
14884 text,
14885 runs: Vec::new(),
14886 filter_range: 0..len,
14887 })
14888 })),
14889 ..FakeLspAdapter::default()
14890 },
14891 );
14892 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14893 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14894 let worktree_id = workspace
14895 .update(cx, |workspace, _window, cx| {
14896 workspace.project().update(cx, |project, cx| {
14897 project.worktrees(cx).next().unwrap().read(cx).id()
14898 })
14899 })
14900 .unwrap();
14901 let _buffer = project
14902 .update(cx, |project, cx| {
14903 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14904 })
14905 .await
14906 .unwrap();
14907 let editor = workspace
14908 .update(cx, |workspace, window, cx| {
14909 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
14910 })
14911 .unwrap()
14912 .await
14913 .unwrap()
14914 .downcast::<Editor>()
14915 .unwrap();
14916 let fake_server = fake_servers.next().await.unwrap();
14917
14918 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14919 let multiline_label_2 = "a\nb\nc\n";
14920 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14921 let multiline_description = "d\ne\nf\n";
14922 let multiline_detail_2 = "g\nh\ni\n";
14923
14924 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14925 move |params, _| async move {
14926 Ok(Some(lsp::CompletionResponse::Array(vec![
14927 lsp::CompletionItem {
14928 label: multiline_label.to_string(),
14929 text_edit: gen_text_edit(¶ms, "new_text_1"),
14930 ..lsp::CompletionItem::default()
14931 },
14932 lsp::CompletionItem {
14933 label: "single line label 1".to_string(),
14934 detail: Some(multiline_detail.to_string()),
14935 text_edit: gen_text_edit(¶ms, "new_text_2"),
14936 ..lsp::CompletionItem::default()
14937 },
14938 lsp::CompletionItem {
14939 label: "single line label 2".to_string(),
14940 label_details: Some(lsp::CompletionItemLabelDetails {
14941 description: Some(multiline_description.to_string()),
14942 detail: None,
14943 }),
14944 text_edit: gen_text_edit(¶ms, "new_text_2"),
14945 ..lsp::CompletionItem::default()
14946 },
14947 lsp::CompletionItem {
14948 label: multiline_label_2.to_string(),
14949 detail: Some(multiline_detail_2.to_string()),
14950 text_edit: gen_text_edit(¶ms, "new_text_3"),
14951 ..lsp::CompletionItem::default()
14952 },
14953 lsp::CompletionItem {
14954 label: "Label with many spaces and \t but without newlines".to_string(),
14955 detail: Some(
14956 "Details with many spaces and \t but without newlines".to_string(),
14957 ),
14958 text_edit: gen_text_edit(¶ms, "new_text_4"),
14959 ..lsp::CompletionItem::default()
14960 },
14961 ])))
14962 },
14963 );
14964
14965 editor.update_in(cx, |editor, window, cx| {
14966 cx.focus_self(window);
14967 editor.move_to_end(&MoveToEnd, window, cx);
14968 editor.handle_input(".", window, cx);
14969 });
14970 cx.run_until_parked();
14971 completion_handle.next().await.unwrap();
14972
14973 editor.update(cx, |editor, _| {
14974 assert!(editor.context_menu_visible());
14975 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14976 {
14977 let completion_labels = menu
14978 .completions
14979 .borrow()
14980 .iter()
14981 .map(|c| c.label.text.clone())
14982 .collect::<Vec<_>>();
14983 assert_eq!(
14984 completion_labels,
14985 &[
14986 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14987 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14988 "single line label 2 d e f ",
14989 "a b c g h i ",
14990 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14991 ],
14992 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14993 );
14994
14995 for completion in menu
14996 .completions
14997 .borrow()
14998 .iter() {
14999 assert_eq!(
15000 completion.label.filter_range,
15001 0..completion.label.text.len(),
15002 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15003 );
15004 }
15005 } else {
15006 panic!("expected completion menu to be open");
15007 }
15008 });
15009}
15010
15011#[gpui::test]
15012async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15013 init_test(cx, |_| {});
15014 let mut cx = EditorLspTestContext::new_rust(
15015 lsp::ServerCapabilities {
15016 completion_provider: Some(lsp::CompletionOptions {
15017 trigger_characters: Some(vec![".".to_string()]),
15018 ..Default::default()
15019 }),
15020 ..Default::default()
15021 },
15022 cx,
15023 )
15024 .await;
15025 cx.lsp
15026 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15027 Ok(Some(lsp::CompletionResponse::Array(vec![
15028 lsp::CompletionItem {
15029 label: "first".into(),
15030 ..Default::default()
15031 },
15032 lsp::CompletionItem {
15033 label: "last".into(),
15034 ..Default::default()
15035 },
15036 ])))
15037 });
15038 cx.set_state("variableˇ");
15039 cx.simulate_keystroke(".");
15040 cx.executor().run_until_parked();
15041
15042 cx.update_editor(|editor, _, _| {
15043 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15044 {
15045 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15046 } else {
15047 panic!("expected completion menu to be open");
15048 }
15049 });
15050
15051 cx.update_editor(|editor, window, cx| {
15052 editor.move_page_down(&MovePageDown::default(), window, cx);
15053 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15054 {
15055 assert!(
15056 menu.selected_item == 1,
15057 "expected PageDown to select the last item from the context menu"
15058 );
15059 } else {
15060 panic!("expected completion menu to stay open after PageDown");
15061 }
15062 });
15063
15064 cx.update_editor(|editor, window, cx| {
15065 editor.move_page_up(&MovePageUp::default(), window, cx);
15066 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15067 {
15068 assert!(
15069 menu.selected_item == 0,
15070 "expected PageUp to select the first item from the context menu"
15071 );
15072 } else {
15073 panic!("expected completion menu to stay open after PageUp");
15074 }
15075 });
15076}
15077
15078#[gpui::test]
15079async fn test_as_is_completions(cx: &mut TestAppContext) {
15080 init_test(cx, |_| {});
15081 let mut cx = EditorLspTestContext::new_rust(
15082 lsp::ServerCapabilities {
15083 completion_provider: Some(lsp::CompletionOptions {
15084 ..Default::default()
15085 }),
15086 ..Default::default()
15087 },
15088 cx,
15089 )
15090 .await;
15091 cx.lsp
15092 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15093 Ok(Some(lsp::CompletionResponse::Array(vec![
15094 lsp::CompletionItem {
15095 label: "unsafe".into(),
15096 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15097 range: lsp::Range {
15098 start: lsp::Position {
15099 line: 1,
15100 character: 2,
15101 },
15102 end: lsp::Position {
15103 line: 1,
15104 character: 3,
15105 },
15106 },
15107 new_text: "unsafe".to_string(),
15108 })),
15109 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15110 ..Default::default()
15111 },
15112 ])))
15113 });
15114 cx.set_state("fn a() {}\n nˇ");
15115 cx.executor().run_until_parked();
15116 cx.update_editor(|editor, window, cx| {
15117 editor.show_completions(
15118 &ShowCompletions {
15119 trigger: Some("\n".into()),
15120 },
15121 window,
15122 cx,
15123 );
15124 });
15125 cx.executor().run_until_parked();
15126
15127 cx.update_editor(|editor, window, cx| {
15128 editor.confirm_completion(&Default::default(), window, cx)
15129 });
15130 cx.executor().run_until_parked();
15131 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15132}
15133
15134#[gpui::test]
15135async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15136 init_test(cx, |_| {});
15137 let language =
15138 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15139 let mut cx = EditorLspTestContext::new(
15140 language,
15141 lsp::ServerCapabilities {
15142 completion_provider: Some(lsp::CompletionOptions {
15143 ..lsp::CompletionOptions::default()
15144 }),
15145 ..lsp::ServerCapabilities::default()
15146 },
15147 cx,
15148 )
15149 .await;
15150
15151 cx.set_state(
15152 "#ifndef BAR_H
15153#define BAR_H
15154
15155#include <stdbool.h>
15156
15157int fn_branch(bool do_branch1, bool do_branch2);
15158
15159#endif // BAR_H
15160ˇ",
15161 );
15162 cx.executor().run_until_parked();
15163 cx.update_editor(|editor, window, cx| {
15164 editor.handle_input("#", window, cx);
15165 });
15166 cx.executor().run_until_parked();
15167 cx.update_editor(|editor, window, cx| {
15168 editor.handle_input("i", window, cx);
15169 });
15170 cx.executor().run_until_parked();
15171 cx.update_editor(|editor, window, cx| {
15172 editor.handle_input("n", window, cx);
15173 });
15174 cx.executor().run_until_parked();
15175 cx.assert_editor_state(
15176 "#ifndef BAR_H
15177#define BAR_H
15178
15179#include <stdbool.h>
15180
15181int fn_branch(bool do_branch1, bool do_branch2);
15182
15183#endif // BAR_H
15184#inˇ",
15185 );
15186
15187 cx.lsp
15188 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15189 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15190 is_incomplete: false,
15191 item_defaults: None,
15192 items: vec![lsp::CompletionItem {
15193 kind: Some(lsp::CompletionItemKind::SNIPPET),
15194 label_details: Some(lsp::CompletionItemLabelDetails {
15195 detail: Some("header".to_string()),
15196 description: None,
15197 }),
15198 label: " include".to_string(),
15199 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15200 range: lsp::Range {
15201 start: lsp::Position {
15202 line: 8,
15203 character: 1,
15204 },
15205 end: lsp::Position {
15206 line: 8,
15207 character: 1,
15208 },
15209 },
15210 new_text: "include \"$0\"".to_string(),
15211 })),
15212 sort_text: Some("40b67681include".to_string()),
15213 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15214 filter_text: Some("include".to_string()),
15215 insert_text: Some("include \"$0\"".to_string()),
15216 ..lsp::CompletionItem::default()
15217 }],
15218 })))
15219 });
15220 cx.update_editor(|editor, window, cx| {
15221 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15222 });
15223 cx.executor().run_until_parked();
15224 cx.update_editor(|editor, window, cx| {
15225 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15226 });
15227 cx.executor().run_until_parked();
15228 cx.assert_editor_state(
15229 "#ifndef BAR_H
15230#define BAR_H
15231
15232#include <stdbool.h>
15233
15234int fn_branch(bool do_branch1, bool do_branch2);
15235
15236#endif // BAR_H
15237#include \"ˇ\"",
15238 );
15239
15240 cx.lsp
15241 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15242 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15243 is_incomplete: true,
15244 item_defaults: None,
15245 items: vec![lsp::CompletionItem {
15246 kind: Some(lsp::CompletionItemKind::FILE),
15247 label: "AGL/".to_string(),
15248 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15249 range: lsp::Range {
15250 start: lsp::Position {
15251 line: 8,
15252 character: 10,
15253 },
15254 end: lsp::Position {
15255 line: 8,
15256 character: 11,
15257 },
15258 },
15259 new_text: "AGL/".to_string(),
15260 })),
15261 sort_text: Some("40b67681AGL/".to_string()),
15262 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15263 filter_text: Some("AGL/".to_string()),
15264 insert_text: Some("AGL/".to_string()),
15265 ..lsp::CompletionItem::default()
15266 }],
15267 })))
15268 });
15269 cx.update_editor(|editor, window, cx| {
15270 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15271 });
15272 cx.executor().run_until_parked();
15273 cx.update_editor(|editor, window, cx| {
15274 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15275 });
15276 cx.executor().run_until_parked();
15277 cx.assert_editor_state(
15278 r##"#ifndef BAR_H
15279#define BAR_H
15280
15281#include <stdbool.h>
15282
15283int fn_branch(bool do_branch1, bool do_branch2);
15284
15285#endif // BAR_H
15286#include "AGL/ˇ"##,
15287 );
15288
15289 cx.update_editor(|editor, window, cx| {
15290 editor.handle_input("\"", window, cx);
15291 });
15292 cx.executor().run_until_parked();
15293 cx.assert_editor_state(
15294 r##"#ifndef BAR_H
15295#define BAR_H
15296
15297#include <stdbool.h>
15298
15299int fn_branch(bool do_branch1, bool do_branch2);
15300
15301#endif // BAR_H
15302#include "AGL/"ˇ"##,
15303 );
15304}
15305
15306#[gpui::test]
15307async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15308 init_test(cx, |_| {});
15309
15310 let mut cx = EditorLspTestContext::new_rust(
15311 lsp::ServerCapabilities {
15312 completion_provider: Some(lsp::CompletionOptions {
15313 trigger_characters: Some(vec![".".to_string()]),
15314 resolve_provider: Some(true),
15315 ..Default::default()
15316 }),
15317 ..Default::default()
15318 },
15319 cx,
15320 )
15321 .await;
15322
15323 cx.set_state("fn main() { let a = 2ˇ; }");
15324 cx.simulate_keystroke(".");
15325 let completion_item = lsp::CompletionItem {
15326 label: "Some".into(),
15327 kind: Some(lsp::CompletionItemKind::SNIPPET),
15328 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15329 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15330 kind: lsp::MarkupKind::Markdown,
15331 value: "```rust\nSome(2)\n```".to_string(),
15332 })),
15333 deprecated: Some(false),
15334 sort_text: Some("Some".to_string()),
15335 filter_text: Some("Some".to_string()),
15336 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15337 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15338 range: lsp::Range {
15339 start: lsp::Position {
15340 line: 0,
15341 character: 22,
15342 },
15343 end: lsp::Position {
15344 line: 0,
15345 character: 22,
15346 },
15347 },
15348 new_text: "Some(2)".to_string(),
15349 })),
15350 additional_text_edits: Some(vec![lsp::TextEdit {
15351 range: lsp::Range {
15352 start: lsp::Position {
15353 line: 0,
15354 character: 20,
15355 },
15356 end: lsp::Position {
15357 line: 0,
15358 character: 22,
15359 },
15360 },
15361 new_text: "".to_string(),
15362 }]),
15363 ..Default::default()
15364 };
15365
15366 let closure_completion_item = completion_item.clone();
15367 let counter = Arc::new(AtomicUsize::new(0));
15368 let counter_clone = counter.clone();
15369 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15370 let task_completion_item = closure_completion_item.clone();
15371 counter_clone.fetch_add(1, atomic::Ordering::Release);
15372 async move {
15373 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15374 is_incomplete: true,
15375 item_defaults: None,
15376 items: vec![task_completion_item],
15377 })))
15378 }
15379 });
15380
15381 cx.condition(|editor, _| editor.context_menu_visible())
15382 .await;
15383 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15384 assert!(request.next().await.is_some());
15385 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15386
15387 cx.simulate_keystrokes("S o m");
15388 cx.condition(|editor, _| editor.context_menu_visible())
15389 .await;
15390 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15391 assert!(request.next().await.is_some());
15392 assert!(request.next().await.is_some());
15393 assert!(request.next().await.is_some());
15394 request.close();
15395 assert!(request.next().await.is_none());
15396 assert_eq!(
15397 counter.load(atomic::Ordering::Acquire),
15398 4,
15399 "With the completions menu open, only one LSP request should happen per input"
15400 );
15401}
15402
15403#[gpui::test]
15404async fn test_toggle_comment(cx: &mut TestAppContext) {
15405 init_test(cx, |_| {});
15406 let mut cx = EditorTestContext::new(cx).await;
15407 let language = Arc::new(Language::new(
15408 LanguageConfig {
15409 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15410 ..Default::default()
15411 },
15412 Some(tree_sitter_rust::LANGUAGE.into()),
15413 ));
15414 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15415
15416 // If multiple selections intersect a line, the line is only toggled once.
15417 cx.set_state(indoc! {"
15418 fn a() {
15419 «//b();
15420 ˇ»// «c();
15421 //ˇ» d();
15422 }
15423 "});
15424
15425 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15426
15427 cx.assert_editor_state(indoc! {"
15428 fn a() {
15429 «b();
15430 c();
15431 ˇ» d();
15432 }
15433 "});
15434
15435 // The comment prefix is inserted at the same column for every line in a
15436 // selection.
15437 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15438
15439 cx.assert_editor_state(indoc! {"
15440 fn a() {
15441 // «b();
15442 // c();
15443 ˇ»// d();
15444 }
15445 "});
15446
15447 // If a selection ends at the beginning of a line, that line is not toggled.
15448 cx.set_selections_state(indoc! {"
15449 fn a() {
15450 // b();
15451 «// c();
15452 ˇ» // d();
15453 }
15454 "});
15455
15456 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15457
15458 cx.assert_editor_state(indoc! {"
15459 fn a() {
15460 // b();
15461 «c();
15462 ˇ» // d();
15463 }
15464 "});
15465
15466 // If a selection span a single line and is empty, the line is toggled.
15467 cx.set_state(indoc! {"
15468 fn a() {
15469 a();
15470 b();
15471 ˇ
15472 }
15473 "});
15474
15475 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15476
15477 cx.assert_editor_state(indoc! {"
15478 fn a() {
15479 a();
15480 b();
15481 //•ˇ
15482 }
15483 "});
15484
15485 // If a selection span multiple lines, empty lines are not toggled.
15486 cx.set_state(indoc! {"
15487 fn a() {
15488 «a();
15489
15490 c();ˇ»
15491 }
15492 "});
15493
15494 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15495
15496 cx.assert_editor_state(indoc! {"
15497 fn a() {
15498 // «a();
15499
15500 // c();ˇ»
15501 }
15502 "});
15503
15504 // If a selection includes multiple comment prefixes, all lines are uncommented.
15505 cx.set_state(indoc! {"
15506 fn a() {
15507 «// a();
15508 /// b();
15509 //! c();ˇ»
15510 }
15511 "});
15512
15513 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15514
15515 cx.assert_editor_state(indoc! {"
15516 fn a() {
15517 «a();
15518 b();
15519 c();ˇ»
15520 }
15521 "});
15522}
15523
15524#[gpui::test]
15525async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15526 init_test(cx, |_| {});
15527 let mut cx = EditorTestContext::new(cx).await;
15528 let language = Arc::new(Language::new(
15529 LanguageConfig {
15530 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15531 ..Default::default()
15532 },
15533 Some(tree_sitter_rust::LANGUAGE.into()),
15534 ));
15535 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15536
15537 let toggle_comments = &ToggleComments {
15538 advance_downwards: false,
15539 ignore_indent: true,
15540 };
15541
15542 // If multiple selections intersect a line, the line is only toggled once.
15543 cx.set_state(indoc! {"
15544 fn a() {
15545 // «b();
15546 // c();
15547 // ˇ» d();
15548 }
15549 "});
15550
15551 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15552
15553 cx.assert_editor_state(indoc! {"
15554 fn a() {
15555 «b();
15556 c();
15557 ˇ» d();
15558 }
15559 "});
15560
15561 // The comment prefix is inserted at the beginning of each line
15562 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15563
15564 cx.assert_editor_state(indoc! {"
15565 fn a() {
15566 // «b();
15567 // c();
15568 // ˇ» d();
15569 }
15570 "});
15571
15572 // If a selection ends at the beginning of a line, that line is not toggled.
15573 cx.set_selections_state(indoc! {"
15574 fn a() {
15575 // b();
15576 // «c();
15577 ˇ»// d();
15578 }
15579 "});
15580
15581 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15582
15583 cx.assert_editor_state(indoc! {"
15584 fn a() {
15585 // b();
15586 «c();
15587 ˇ»// d();
15588 }
15589 "});
15590
15591 // If a selection span a single line and is empty, the line is toggled.
15592 cx.set_state(indoc! {"
15593 fn a() {
15594 a();
15595 b();
15596 ˇ
15597 }
15598 "});
15599
15600 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15601
15602 cx.assert_editor_state(indoc! {"
15603 fn a() {
15604 a();
15605 b();
15606 //ˇ
15607 }
15608 "});
15609
15610 // If a selection span multiple lines, empty lines are not toggled.
15611 cx.set_state(indoc! {"
15612 fn a() {
15613 «a();
15614
15615 c();ˇ»
15616 }
15617 "});
15618
15619 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15620
15621 cx.assert_editor_state(indoc! {"
15622 fn a() {
15623 // «a();
15624
15625 // c();ˇ»
15626 }
15627 "});
15628
15629 // If a selection includes multiple comment prefixes, all lines are uncommented.
15630 cx.set_state(indoc! {"
15631 fn a() {
15632 // «a();
15633 /// b();
15634 //! c();ˇ»
15635 }
15636 "});
15637
15638 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15639
15640 cx.assert_editor_state(indoc! {"
15641 fn a() {
15642 «a();
15643 b();
15644 c();ˇ»
15645 }
15646 "});
15647}
15648
15649#[gpui::test]
15650async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15651 init_test(cx, |_| {});
15652
15653 let language = Arc::new(Language::new(
15654 LanguageConfig {
15655 line_comments: vec!["// ".into()],
15656 ..Default::default()
15657 },
15658 Some(tree_sitter_rust::LANGUAGE.into()),
15659 ));
15660
15661 let mut cx = EditorTestContext::new(cx).await;
15662
15663 cx.language_registry().add(language.clone());
15664 cx.update_buffer(|buffer, cx| {
15665 buffer.set_language(Some(language), cx);
15666 });
15667
15668 let toggle_comments = &ToggleComments {
15669 advance_downwards: true,
15670 ignore_indent: false,
15671 };
15672
15673 // Single cursor on one line -> advance
15674 // Cursor moves horizontally 3 characters as well on non-blank line
15675 cx.set_state(indoc!(
15676 "fn a() {
15677 ˇdog();
15678 cat();
15679 }"
15680 ));
15681 cx.update_editor(|editor, window, cx| {
15682 editor.toggle_comments(toggle_comments, window, cx);
15683 });
15684 cx.assert_editor_state(indoc!(
15685 "fn a() {
15686 // dog();
15687 catˇ();
15688 }"
15689 ));
15690
15691 // Single selection on one line -> don't advance
15692 cx.set_state(indoc!(
15693 "fn a() {
15694 «dog()ˇ»;
15695 cat();
15696 }"
15697 ));
15698 cx.update_editor(|editor, window, cx| {
15699 editor.toggle_comments(toggle_comments, window, cx);
15700 });
15701 cx.assert_editor_state(indoc!(
15702 "fn a() {
15703 // «dog()ˇ»;
15704 cat();
15705 }"
15706 ));
15707
15708 // Multiple cursors on one line -> advance
15709 cx.set_state(indoc!(
15710 "fn a() {
15711 ˇdˇog();
15712 cat();
15713 }"
15714 ));
15715 cx.update_editor(|editor, window, cx| {
15716 editor.toggle_comments(toggle_comments, window, cx);
15717 });
15718 cx.assert_editor_state(indoc!(
15719 "fn a() {
15720 // dog();
15721 catˇ(ˇ);
15722 }"
15723 ));
15724
15725 // Multiple cursors on one line, with selection -> don't advance
15726 cx.set_state(indoc!(
15727 "fn a() {
15728 ˇdˇog«()ˇ»;
15729 cat();
15730 }"
15731 ));
15732 cx.update_editor(|editor, window, cx| {
15733 editor.toggle_comments(toggle_comments, window, cx);
15734 });
15735 cx.assert_editor_state(indoc!(
15736 "fn a() {
15737 // ˇdˇog«()ˇ»;
15738 cat();
15739 }"
15740 ));
15741
15742 // Single cursor on one line -> advance
15743 // Cursor moves to column 0 on blank line
15744 cx.set_state(indoc!(
15745 "fn a() {
15746 ˇdog();
15747
15748 cat();
15749 }"
15750 ));
15751 cx.update_editor(|editor, window, cx| {
15752 editor.toggle_comments(toggle_comments, window, cx);
15753 });
15754 cx.assert_editor_state(indoc!(
15755 "fn a() {
15756 // dog();
15757 ˇ
15758 cat();
15759 }"
15760 ));
15761
15762 // Single cursor on one line -> advance
15763 // Cursor starts and ends at column 0
15764 cx.set_state(indoc!(
15765 "fn a() {
15766 ˇ dog();
15767 cat();
15768 }"
15769 ));
15770 cx.update_editor(|editor, window, cx| {
15771 editor.toggle_comments(toggle_comments, window, cx);
15772 });
15773 cx.assert_editor_state(indoc!(
15774 "fn a() {
15775 // dog();
15776 ˇ cat();
15777 }"
15778 ));
15779}
15780
15781#[gpui::test]
15782async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15783 init_test(cx, |_| {});
15784
15785 let mut cx = EditorTestContext::new(cx).await;
15786
15787 let html_language = Arc::new(
15788 Language::new(
15789 LanguageConfig {
15790 name: "HTML".into(),
15791 block_comment: Some(BlockCommentConfig {
15792 start: "<!-- ".into(),
15793 prefix: "".into(),
15794 end: " -->".into(),
15795 tab_size: 0,
15796 }),
15797 ..Default::default()
15798 },
15799 Some(tree_sitter_html::LANGUAGE.into()),
15800 )
15801 .with_injection_query(
15802 r#"
15803 (script_element
15804 (raw_text) @injection.content
15805 (#set! injection.language "javascript"))
15806 "#,
15807 )
15808 .unwrap(),
15809 );
15810
15811 let javascript_language = Arc::new(Language::new(
15812 LanguageConfig {
15813 name: "JavaScript".into(),
15814 line_comments: vec!["// ".into()],
15815 ..Default::default()
15816 },
15817 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15818 ));
15819
15820 cx.language_registry().add(html_language.clone());
15821 cx.language_registry().add(javascript_language);
15822 cx.update_buffer(|buffer, cx| {
15823 buffer.set_language(Some(html_language), cx);
15824 });
15825
15826 // Toggle comments for empty selections
15827 cx.set_state(
15828 &r#"
15829 <p>A</p>ˇ
15830 <p>B</p>ˇ
15831 <p>C</p>ˇ
15832 "#
15833 .unindent(),
15834 );
15835 cx.update_editor(|editor, window, cx| {
15836 editor.toggle_comments(&ToggleComments::default(), window, cx)
15837 });
15838 cx.assert_editor_state(
15839 &r#"
15840 <!-- <p>A</p>ˇ -->
15841 <!-- <p>B</p>ˇ -->
15842 <!-- <p>C</p>ˇ -->
15843 "#
15844 .unindent(),
15845 );
15846 cx.update_editor(|editor, window, cx| {
15847 editor.toggle_comments(&ToggleComments::default(), window, cx)
15848 });
15849 cx.assert_editor_state(
15850 &r#"
15851 <p>A</p>ˇ
15852 <p>B</p>ˇ
15853 <p>C</p>ˇ
15854 "#
15855 .unindent(),
15856 );
15857
15858 // Toggle comments for mixture of empty and non-empty selections, where
15859 // multiple selections occupy a given line.
15860 cx.set_state(
15861 &r#"
15862 <p>A«</p>
15863 <p>ˇ»B</p>ˇ
15864 <p>C«</p>
15865 <p>ˇ»D</p>ˇ
15866 "#
15867 .unindent(),
15868 );
15869
15870 cx.update_editor(|editor, window, cx| {
15871 editor.toggle_comments(&ToggleComments::default(), window, cx)
15872 });
15873 cx.assert_editor_state(
15874 &r#"
15875 <!-- <p>A«</p>
15876 <p>ˇ»B</p>ˇ -->
15877 <!-- <p>C«</p>
15878 <p>ˇ»D</p>ˇ -->
15879 "#
15880 .unindent(),
15881 );
15882 cx.update_editor(|editor, window, cx| {
15883 editor.toggle_comments(&ToggleComments::default(), window, cx)
15884 });
15885 cx.assert_editor_state(
15886 &r#"
15887 <p>A«</p>
15888 <p>ˇ»B</p>ˇ
15889 <p>C«</p>
15890 <p>ˇ»D</p>ˇ
15891 "#
15892 .unindent(),
15893 );
15894
15895 // Toggle comments when different languages are active for different
15896 // selections.
15897 cx.set_state(
15898 &r#"
15899 ˇ<script>
15900 ˇvar x = new Y();
15901 ˇ</script>
15902 "#
15903 .unindent(),
15904 );
15905 cx.executor().run_until_parked();
15906 cx.update_editor(|editor, window, cx| {
15907 editor.toggle_comments(&ToggleComments::default(), window, cx)
15908 });
15909 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15910 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15911 cx.assert_editor_state(
15912 &r#"
15913 <!-- ˇ<script> -->
15914 // ˇvar x = new Y();
15915 <!-- ˇ</script> -->
15916 "#
15917 .unindent(),
15918 );
15919}
15920
15921#[gpui::test]
15922fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15923 init_test(cx, |_| {});
15924
15925 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15926 let multibuffer = cx.new(|cx| {
15927 let mut multibuffer = MultiBuffer::new(ReadWrite);
15928 multibuffer.push_excerpts(
15929 buffer.clone(),
15930 [
15931 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15932 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15933 ],
15934 cx,
15935 );
15936 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15937 multibuffer
15938 });
15939
15940 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15941 editor.update_in(cx, |editor, window, cx| {
15942 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15943 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15944 s.select_ranges([
15945 Point::new(0, 0)..Point::new(0, 0),
15946 Point::new(1, 0)..Point::new(1, 0),
15947 ])
15948 });
15949
15950 editor.handle_input("X", window, cx);
15951 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15952 assert_eq!(
15953 editor.selections.ranges(cx),
15954 [
15955 Point::new(0, 1)..Point::new(0, 1),
15956 Point::new(1, 1)..Point::new(1, 1),
15957 ]
15958 );
15959
15960 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15961 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15962 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15963 });
15964 editor.backspace(&Default::default(), window, cx);
15965 assert_eq!(editor.text(cx), "Xa\nbbb");
15966 assert_eq!(
15967 editor.selections.ranges(cx),
15968 [Point::new(1, 0)..Point::new(1, 0)]
15969 );
15970
15971 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15972 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15973 });
15974 editor.backspace(&Default::default(), window, cx);
15975 assert_eq!(editor.text(cx), "X\nbb");
15976 assert_eq!(
15977 editor.selections.ranges(cx),
15978 [Point::new(0, 1)..Point::new(0, 1)]
15979 );
15980 });
15981}
15982
15983#[gpui::test]
15984fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15985 init_test(cx, |_| {});
15986
15987 let markers = vec![('[', ']').into(), ('(', ')').into()];
15988 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15989 indoc! {"
15990 [aaaa
15991 (bbbb]
15992 cccc)",
15993 },
15994 markers.clone(),
15995 );
15996 let excerpt_ranges = markers.into_iter().map(|marker| {
15997 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15998 ExcerptRange::new(context)
15999 });
16000 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16001 let multibuffer = cx.new(|cx| {
16002 let mut multibuffer = MultiBuffer::new(ReadWrite);
16003 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16004 multibuffer
16005 });
16006
16007 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16008 editor.update_in(cx, |editor, window, cx| {
16009 let (expected_text, selection_ranges) = marked_text_ranges(
16010 indoc! {"
16011 aaaa
16012 bˇbbb
16013 bˇbbˇb
16014 cccc"
16015 },
16016 true,
16017 );
16018 assert_eq!(editor.text(cx), expected_text);
16019 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16020 s.select_ranges(selection_ranges)
16021 });
16022
16023 editor.handle_input("X", window, cx);
16024
16025 let (expected_text, expected_selections) = marked_text_ranges(
16026 indoc! {"
16027 aaaa
16028 bXˇbbXb
16029 bXˇbbXˇb
16030 cccc"
16031 },
16032 false,
16033 );
16034 assert_eq!(editor.text(cx), expected_text);
16035 assert_eq!(editor.selections.ranges(cx), expected_selections);
16036
16037 editor.newline(&Newline, window, cx);
16038 let (expected_text, expected_selections) = marked_text_ranges(
16039 indoc! {"
16040 aaaa
16041 bX
16042 ˇbbX
16043 b
16044 bX
16045 ˇbbX
16046 ˇb
16047 cccc"
16048 },
16049 false,
16050 );
16051 assert_eq!(editor.text(cx), expected_text);
16052 assert_eq!(editor.selections.ranges(cx), expected_selections);
16053 });
16054}
16055
16056#[gpui::test]
16057fn test_refresh_selections(cx: &mut TestAppContext) {
16058 init_test(cx, |_| {});
16059
16060 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16061 let mut excerpt1_id = None;
16062 let multibuffer = cx.new(|cx| {
16063 let mut multibuffer = MultiBuffer::new(ReadWrite);
16064 excerpt1_id = multibuffer
16065 .push_excerpts(
16066 buffer.clone(),
16067 [
16068 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16069 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16070 ],
16071 cx,
16072 )
16073 .into_iter()
16074 .next();
16075 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16076 multibuffer
16077 });
16078
16079 let editor = cx.add_window(|window, cx| {
16080 let mut editor = build_editor(multibuffer.clone(), window, cx);
16081 let snapshot = editor.snapshot(window, cx);
16082 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16083 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16084 });
16085 editor.begin_selection(
16086 Point::new(2, 1).to_display_point(&snapshot),
16087 true,
16088 1,
16089 window,
16090 cx,
16091 );
16092 assert_eq!(
16093 editor.selections.ranges(cx),
16094 [
16095 Point::new(1, 3)..Point::new(1, 3),
16096 Point::new(2, 1)..Point::new(2, 1),
16097 ]
16098 );
16099 editor
16100 });
16101
16102 // Refreshing selections is a no-op when excerpts haven't changed.
16103 _ = editor.update(cx, |editor, window, cx| {
16104 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16105 assert_eq!(
16106 editor.selections.ranges(cx),
16107 [
16108 Point::new(1, 3)..Point::new(1, 3),
16109 Point::new(2, 1)..Point::new(2, 1),
16110 ]
16111 );
16112 });
16113
16114 multibuffer.update(cx, |multibuffer, cx| {
16115 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16116 });
16117 _ = editor.update(cx, |editor, window, cx| {
16118 // Removing an excerpt causes the first selection to become degenerate.
16119 assert_eq!(
16120 editor.selections.ranges(cx),
16121 [
16122 Point::new(0, 0)..Point::new(0, 0),
16123 Point::new(0, 1)..Point::new(0, 1)
16124 ]
16125 );
16126
16127 // Refreshing selections will relocate the first selection to the original buffer
16128 // location.
16129 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16130 assert_eq!(
16131 editor.selections.ranges(cx),
16132 [
16133 Point::new(0, 1)..Point::new(0, 1),
16134 Point::new(0, 3)..Point::new(0, 3)
16135 ]
16136 );
16137 assert!(editor.selections.pending_anchor().is_some());
16138 });
16139}
16140
16141#[gpui::test]
16142fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16143 init_test(cx, |_| {});
16144
16145 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16146 let mut excerpt1_id = None;
16147 let multibuffer = cx.new(|cx| {
16148 let mut multibuffer = MultiBuffer::new(ReadWrite);
16149 excerpt1_id = multibuffer
16150 .push_excerpts(
16151 buffer.clone(),
16152 [
16153 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16154 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16155 ],
16156 cx,
16157 )
16158 .into_iter()
16159 .next();
16160 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16161 multibuffer
16162 });
16163
16164 let editor = cx.add_window(|window, cx| {
16165 let mut editor = build_editor(multibuffer.clone(), window, cx);
16166 let snapshot = editor.snapshot(window, cx);
16167 editor.begin_selection(
16168 Point::new(1, 3).to_display_point(&snapshot),
16169 false,
16170 1,
16171 window,
16172 cx,
16173 );
16174 assert_eq!(
16175 editor.selections.ranges(cx),
16176 [Point::new(1, 3)..Point::new(1, 3)]
16177 );
16178 editor
16179 });
16180
16181 multibuffer.update(cx, |multibuffer, cx| {
16182 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16183 });
16184 _ = editor.update(cx, |editor, window, cx| {
16185 assert_eq!(
16186 editor.selections.ranges(cx),
16187 [Point::new(0, 0)..Point::new(0, 0)]
16188 );
16189
16190 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16191 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16192 assert_eq!(
16193 editor.selections.ranges(cx),
16194 [Point::new(0, 3)..Point::new(0, 3)]
16195 );
16196 assert!(editor.selections.pending_anchor().is_some());
16197 });
16198}
16199
16200#[gpui::test]
16201async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16202 init_test(cx, |_| {});
16203
16204 let language = Arc::new(
16205 Language::new(
16206 LanguageConfig {
16207 brackets: BracketPairConfig {
16208 pairs: vec![
16209 BracketPair {
16210 start: "{".to_string(),
16211 end: "}".to_string(),
16212 close: true,
16213 surround: true,
16214 newline: true,
16215 },
16216 BracketPair {
16217 start: "/* ".to_string(),
16218 end: " */".to_string(),
16219 close: true,
16220 surround: true,
16221 newline: true,
16222 },
16223 ],
16224 ..Default::default()
16225 },
16226 ..Default::default()
16227 },
16228 Some(tree_sitter_rust::LANGUAGE.into()),
16229 )
16230 .with_indents_query("")
16231 .unwrap(),
16232 );
16233
16234 let text = concat!(
16235 "{ }\n", //
16236 " x\n", //
16237 " /* */\n", //
16238 "x\n", //
16239 "{{} }\n", //
16240 );
16241
16242 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16243 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16244 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16245 editor
16246 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16247 .await;
16248
16249 editor.update_in(cx, |editor, window, cx| {
16250 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16251 s.select_display_ranges([
16252 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16253 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16254 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16255 ])
16256 });
16257 editor.newline(&Newline, window, cx);
16258
16259 assert_eq!(
16260 editor.buffer().read(cx).read(cx).text(),
16261 concat!(
16262 "{ \n", // Suppress rustfmt
16263 "\n", //
16264 "}\n", //
16265 " x\n", //
16266 " /* \n", //
16267 " \n", //
16268 " */\n", //
16269 "x\n", //
16270 "{{} \n", //
16271 "}\n", //
16272 )
16273 );
16274 });
16275}
16276
16277#[gpui::test]
16278fn test_highlighted_ranges(cx: &mut TestAppContext) {
16279 init_test(cx, |_| {});
16280
16281 let editor = cx.add_window(|window, cx| {
16282 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16283 build_editor(buffer, window, cx)
16284 });
16285
16286 _ = editor.update(cx, |editor, window, cx| {
16287 struct Type1;
16288 struct Type2;
16289
16290 let buffer = editor.buffer.read(cx).snapshot(cx);
16291
16292 let anchor_range =
16293 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16294
16295 editor.highlight_background::<Type1>(
16296 &[
16297 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16298 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16299 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16300 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16301 ],
16302 |_| Hsla::red(),
16303 cx,
16304 );
16305 editor.highlight_background::<Type2>(
16306 &[
16307 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16308 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16309 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16310 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16311 ],
16312 |_| Hsla::green(),
16313 cx,
16314 );
16315
16316 let snapshot = editor.snapshot(window, cx);
16317 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16318 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16319 &snapshot,
16320 cx.theme(),
16321 );
16322 assert_eq!(
16323 highlighted_ranges,
16324 &[
16325 (
16326 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16327 Hsla::green(),
16328 ),
16329 (
16330 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16331 Hsla::red(),
16332 ),
16333 (
16334 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16335 Hsla::green(),
16336 ),
16337 (
16338 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16339 Hsla::red(),
16340 ),
16341 ]
16342 );
16343 assert_eq!(
16344 editor.sorted_background_highlights_in_range(
16345 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16346 &snapshot,
16347 cx.theme(),
16348 ),
16349 &[(
16350 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16351 Hsla::red(),
16352 )]
16353 );
16354 });
16355}
16356
16357#[gpui::test]
16358async fn test_following(cx: &mut TestAppContext) {
16359 init_test(cx, |_| {});
16360
16361 let fs = FakeFs::new(cx.executor());
16362 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16363
16364 let buffer = project.update(cx, |project, cx| {
16365 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16366 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16367 });
16368 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16369 let follower = cx.update(|cx| {
16370 cx.open_window(
16371 WindowOptions {
16372 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16373 gpui::Point::new(px(0.), px(0.)),
16374 gpui::Point::new(px(10.), px(80.)),
16375 ))),
16376 ..Default::default()
16377 },
16378 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16379 )
16380 .unwrap()
16381 });
16382
16383 let is_still_following = Rc::new(RefCell::new(true));
16384 let follower_edit_event_count = Rc::new(RefCell::new(0));
16385 let pending_update = Rc::new(RefCell::new(None));
16386 let leader_entity = leader.root(cx).unwrap();
16387 let follower_entity = follower.root(cx).unwrap();
16388 _ = follower.update(cx, {
16389 let update = pending_update.clone();
16390 let is_still_following = is_still_following.clone();
16391 let follower_edit_event_count = follower_edit_event_count.clone();
16392 |_, window, cx| {
16393 cx.subscribe_in(
16394 &leader_entity,
16395 window,
16396 move |_, leader, event, window, cx| {
16397 leader.read(cx).add_event_to_update_proto(
16398 event,
16399 &mut update.borrow_mut(),
16400 window,
16401 cx,
16402 );
16403 },
16404 )
16405 .detach();
16406
16407 cx.subscribe_in(
16408 &follower_entity,
16409 window,
16410 move |_, _, event: &EditorEvent, _window, _cx| {
16411 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16412 *is_still_following.borrow_mut() = false;
16413 }
16414
16415 if let EditorEvent::BufferEdited = event {
16416 *follower_edit_event_count.borrow_mut() += 1;
16417 }
16418 },
16419 )
16420 .detach();
16421 }
16422 });
16423
16424 // Update the selections only
16425 _ = leader.update(cx, |leader, window, cx| {
16426 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16427 s.select_ranges([1..1])
16428 });
16429 });
16430 follower
16431 .update(cx, |follower, window, cx| {
16432 follower.apply_update_proto(
16433 &project,
16434 pending_update.borrow_mut().take().unwrap(),
16435 window,
16436 cx,
16437 )
16438 })
16439 .unwrap()
16440 .await
16441 .unwrap();
16442 _ = follower.update(cx, |follower, _, cx| {
16443 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
16444 });
16445 assert!(*is_still_following.borrow());
16446 assert_eq!(*follower_edit_event_count.borrow(), 0);
16447
16448 // Update the scroll position only
16449 _ = leader.update(cx, |leader, window, cx| {
16450 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16451 });
16452 follower
16453 .update(cx, |follower, window, cx| {
16454 follower.apply_update_proto(
16455 &project,
16456 pending_update.borrow_mut().take().unwrap(),
16457 window,
16458 cx,
16459 )
16460 })
16461 .unwrap()
16462 .await
16463 .unwrap();
16464 assert_eq!(
16465 follower
16466 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16467 .unwrap(),
16468 gpui::Point::new(1.5, 3.5)
16469 );
16470 assert!(*is_still_following.borrow());
16471 assert_eq!(*follower_edit_event_count.borrow(), 0);
16472
16473 // Update the selections and scroll position. The follower's scroll position is updated
16474 // via autoscroll, not via the leader's exact scroll position.
16475 _ = leader.update(cx, |leader, window, cx| {
16476 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16477 s.select_ranges([0..0])
16478 });
16479 leader.request_autoscroll(Autoscroll::newest(), cx);
16480 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16481 });
16482 follower
16483 .update(cx, |follower, window, cx| {
16484 follower.apply_update_proto(
16485 &project,
16486 pending_update.borrow_mut().take().unwrap(),
16487 window,
16488 cx,
16489 )
16490 })
16491 .unwrap()
16492 .await
16493 .unwrap();
16494 _ = follower.update(cx, |follower, _, cx| {
16495 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16496 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16497 });
16498 assert!(*is_still_following.borrow());
16499
16500 // Creating a pending selection that precedes another selection
16501 _ = leader.update(cx, |leader, window, cx| {
16502 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16503 s.select_ranges([1..1])
16504 });
16505 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16506 });
16507 follower
16508 .update(cx, |follower, window, cx| {
16509 follower.apply_update_proto(
16510 &project,
16511 pending_update.borrow_mut().take().unwrap(),
16512 window,
16513 cx,
16514 )
16515 })
16516 .unwrap()
16517 .await
16518 .unwrap();
16519 _ = follower.update(cx, |follower, _, cx| {
16520 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16521 });
16522 assert!(*is_still_following.borrow());
16523
16524 // Extend the pending selection so that it surrounds another selection
16525 _ = leader.update(cx, |leader, window, cx| {
16526 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16527 });
16528 follower
16529 .update(cx, |follower, window, cx| {
16530 follower.apply_update_proto(
16531 &project,
16532 pending_update.borrow_mut().take().unwrap(),
16533 window,
16534 cx,
16535 )
16536 })
16537 .unwrap()
16538 .await
16539 .unwrap();
16540 _ = follower.update(cx, |follower, _, cx| {
16541 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16542 });
16543
16544 // Scrolling locally breaks the follow
16545 _ = follower.update(cx, |follower, window, cx| {
16546 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16547 follower.set_scroll_anchor(
16548 ScrollAnchor {
16549 anchor: top_anchor,
16550 offset: gpui::Point::new(0.0, 0.5),
16551 },
16552 window,
16553 cx,
16554 );
16555 });
16556 assert!(!(*is_still_following.borrow()));
16557}
16558
16559#[gpui::test]
16560async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16561 init_test(cx, |_| {});
16562
16563 let fs = FakeFs::new(cx.executor());
16564 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16565 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16566 let pane = workspace
16567 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16568 .unwrap();
16569
16570 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16571
16572 let leader = pane.update_in(cx, |_, window, cx| {
16573 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16574 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16575 });
16576
16577 // Start following the editor when it has no excerpts.
16578 let mut state_message =
16579 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16580 let workspace_entity = workspace.root(cx).unwrap();
16581 let follower_1 = cx
16582 .update_window(*workspace.deref(), |_, window, cx| {
16583 Editor::from_state_proto(
16584 workspace_entity,
16585 ViewId {
16586 creator: CollaboratorId::PeerId(PeerId::default()),
16587 id: 0,
16588 },
16589 &mut state_message,
16590 window,
16591 cx,
16592 )
16593 })
16594 .unwrap()
16595 .unwrap()
16596 .await
16597 .unwrap();
16598
16599 let update_message = Rc::new(RefCell::new(None));
16600 follower_1.update_in(cx, {
16601 let update = update_message.clone();
16602 |_, window, cx| {
16603 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16604 leader.read(cx).add_event_to_update_proto(
16605 event,
16606 &mut update.borrow_mut(),
16607 window,
16608 cx,
16609 );
16610 })
16611 .detach();
16612 }
16613 });
16614
16615 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16616 (
16617 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16618 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16619 )
16620 });
16621
16622 // Insert some excerpts.
16623 leader.update(cx, |leader, cx| {
16624 leader.buffer.update(cx, |multibuffer, cx| {
16625 multibuffer.set_excerpts_for_path(
16626 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
16627 buffer_1.clone(),
16628 vec![
16629 Point::row_range(0..3),
16630 Point::row_range(1..6),
16631 Point::row_range(12..15),
16632 ],
16633 0,
16634 cx,
16635 );
16636 multibuffer.set_excerpts_for_path(
16637 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
16638 buffer_2.clone(),
16639 vec![Point::row_range(0..6), Point::row_range(8..12)],
16640 0,
16641 cx,
16642 );
16643 });
16644 });
16645
16646 // Apply the update of adding the excerpts.
16647 follower_1
16648 .update_in(cx, |follower, window, cx| {
16649 follower.apply_update_proto(
16650 &project,
16651 update_message.borrow().clone().unwrap(),
16652 window,
16653 cx,
16654 )
16655 })
16656 .await
16657 .unwrap();
16658 assert_eq!(
16659 follower_1.update(cx, |editor, cx| editor.text(cx)),
16660 leader.update(cx, |editor, cx| editor.text(cx))
16661 );
16662 update_message.borrow_mut().take();
16663
16664 // Start following separately after it already has excerpts.
16665 let mut state_message =
16666 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16667 let workspace_entity = workspace.root(cx).unwrap();
16668 let follower_2 = cx
16669 .update_window(*workspace.deref(), |_, window, cx| {
16670 Editor::from_state_proto(
16671 workspace_entity,
16672 ViewId {
16673 creator: CollaboratorId::PeerId(PeerId::default()),
16674 id: 0,
16675 },
16676 &mut state_message,
16677 window,
16678 cx,
16679 )
16680 })
16681 .unwrap()
16682 .unwrap()
16683 .await
16684 .unwrap();
16685 assert_eq!(
16686 follower_2.update(cx, |editor, cx| editor.text(cx)),
16687 leader.update(cx, |editor, cx| editor.text(cx))
16688 );
16689
16690 // Remove some excerpts.
16691 leader.update(cx, |leader, cx| {
16692 leader.buffer.update(cx, |multibuffer, cx| {
16693 let excerpt_ids = multibuffer.excerpt_ids();
16694 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16695 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16696 });
16697 });
16698
16699 // Apply the update of removing the excerpts.
16700 follower_1
16701 .update_in(cx, |follower, window, cx| {
16702 follower.apply_update_proto(
16703 &project,
16704 update_message.borrow().clone().unwrap(),
16705 window,
16706 cx,
16707 )
16708 })
16709 .await
16710 .unwrap();
16711 follower_2
16712 .update_in(cx, |follower, window, cx| {
16713 follower.apply_update_proto(
16714 &project,
16715 update_message.borrow().clone().unwrap(),
16716 window,
16717 cx,
16718 )
16719 })
16720 .await
16721 .unwrap();
16722 update_message.borrow_mut().take();
16723 assert_eq!(
16724 follower_1.update(cx, |editor, cx| editor.text(cx)),
16725 leader.update(cx, |editor, cx| editor.text(cx))
16726 );
16727}
16728
16729#[gpui::test]
16730async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16731 init_test(cx, |_| {});
16732
16733 let mut cx = EditorTestContext::new(cx).await;
16734 let lsp_store =
16735 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16736
16737 cx.set_state(indoc! {"
16738 ˇfn func(abc def: i32) -> u32 {
16739 }
16740 "});
16741
16742 cx.update(|_, cx| {
16743 lsp_store.update(cx, |lsp_store, cx| {
16744 lsp_store
16745 .update_diagnostics(
16746 LanguageServerId(0),
16747 lsp::PublishDiagnosticsParams {
16748 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16749 version: None,
16750 diagnostics: vec![
16751 lsp::Diagnostic {
16752 range: lsp::Range::new(
16753 lsp::Position::new(0, 11),
16754 lsp::Position::new(0, 12),
16755 ),
16756 severity: Some(lsp::DiagnosticSeverity::ERROR),
16757 ..Default::default()
16758 },
16759 lsp::Diagnostic {
16760 range: lsp::Range::new(
16761 lsp::Position::new(0, 12),
16762 lsp::Position::new(0, 15),
16763 ),
16764 severity: Some(lsp::DiagnosticSeverity::ERROR),
16765 ..Default::default()
16766 },
16767 lsp::Diagnostic {
16768 range: lsp::Range::new(
16769 lsp::Position::new(0, 25),
16770 lsp::Position::new(0, 28),
16771 ),
16772 severity: Some(lsp::DiagnosticSeverity::ERROR),
16773 ..Default::default()
16774 },
16775 ],
16776 },
16777 None,
16778 DiagnosticSourceKind::Pushed,
16779 &[],
16780 cx,
16781 )
16782 .unwrap()
16783 });
16784 });
16785
16786 executor.run_until_parked();
16787
16788 cx.update_editor(|editor, window, cx| {
16789 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16790 });
16791
16792 cx.assert_editor_state(indoc! {"
16793 fn func(abc def: i32) -> ˇu32 {
16794 }
16795 "});
16796
16797 cx.update_editor(|editor, window, cx| {
16798 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16799 });
16800
16801 cx.assert_editor_state(indoc! {"
16802 fn func(abc ˇdef: i32) -> u32 {
16803 }
16804 "});
16805
16806 cx.update_editor(|editor, window, cx| {
16807 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16808 });
16809
16810 cx.assert_editor_state(indoc! {"
16811 fn func(abcˇ def: i32) -> u32 {
16812 }
16813 "});
16814
16815 cx.update_editor(|editor, window, cx| {
16816 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16817 });
16818
16819 cx.assert_editor_state(indoc! {"
16820 fn func(abc def: i32) -> ˇu32 {
16821 }
16822 "});
16823}
16824
16825#[gpui::test]
16826async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16827 init_test(cx, |_| {});
16828
16829 let mut cx = EditorTestContext::new(cx).await;
16830
16831 let diff_base = r#"
16832 use some::mod;
16833
16834 const A: u32 = 42;
16835
16836 fn main() {
16837 println!("hello");
16838
16839 println!("world");
16840 }
16841 "#
16842 .unindent();
16843
16844 // Edits are modified, removed, modified, added
16845 cx.set_state(
16846 &r#"
16847 use some::modified;
16848
16849 ˇ
16850 fn main() {
16851 println!("hello there");
16852
16853 println!("around the");
16854 println!("world");
16855 }
16856 "#
16857 .unindent(),
16858 );
16859
16860 cx.set_head_text(&diff_base);
16861 executor.run_until_parked();
16862
16863 cx.update_editor(|editor, window, cx| {
16864 //Wrap around the bottom of the buffer
16865 for _ in 0..3 {
16866 editor.go_to_next_hunk(&GoToHunk, window, cx);
16867 }
16868 });
16869
16870 cx.assert_editor_state(
16871 &r#"
16872 ˇuse some::modified;
16873
16874
16875 fn main() {
16876 println!("hello there");
16877
16878 println!("around the");
16879 println!("world");
16880 }
16881 "#
16882 .unindent(),
16883 );
16884
16885 cx.update_editor(|editor, window, cx| {
16886 //Wrap around the top of the buffer
16887 for _ in 0..2 {
16888 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16889 }
16890 });
16891
16892 cx.assert_editor_state(
16893 &r#"
16894 use some::modified;
16895
16896
16897 fn main() {
16898 ˇ println!("hello there");
16899
16900 println!("around the");
16901 println!("world");
16902 }
16903 "#
16904 .unindent(),
16905 );
16906
16907 cx.update_editor(|editor, window, cx| {
16908 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16909 });
16910
16911 cx.assert_editor_state(
16912 &r#"
16913 use some::modified;
16914
16915 ˇ
16916 fn main() {
16917 println!("hello there");
16918
16919 println!("around the");
16920 println!("world");
16921 }
16922 "#
16923 .unindent(),
16924 );
16925
16926 cx.update_editor(|editor, window, cx| {
16927 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16928 });
16929
16930 cx.assert_editor_state(
16931 &r#"
16932 ˇuse some::modified;
16933
16934
16935 fn main() {
16936 println!("hello there");
16937
16938 println!("around the");
16939 println!("world");
16940 }
16941 "#
16942 .unindent(),
16943 );
16944
16945 cx.update_editor(|editor, window, cx| {
16946 for _ in 0..2 {
16947 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16948 }
16949 });
16950
16951 cx.assert_editor_state(
16952 &r#"
16953 use some::modified;
16954
16955
16956 fn main() {
16957 ˇ println!("hello there");
16958
16959 println!("around the");
16960 println!("world");
16961 }
16962 "#
16963 .unindent(),
16964 );
16965
16966 cx.update_editor(|editor, window, cx| {
16967 editor.fold(&Fold, window, cx);
16968 });
16969
16970 cx.update_editor(|editor, window, cx| {
16971 editor.go_to_next_hunk(&GoToHunk, window, cx);
16972 });
16973
16974 cx.assert_editor_state(
16975 &r#"
16976 ˇuse some::modified;
16977
16978
16979 fn main() {
16980 println!("hello there");
16981
16982 println!("around the");
16983 println!("world");
16984 }
16985 "#
16986 .unindent(),
16987 );
16988}
16989
16990#[test]
16991fn test_split_words() {
16992 fn split(text: &str) -> Vec<&str> {
16993 split_words(text).collect()
16994 }
16995
16996 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16997 assert_eq!(split("hello_world"), &["hello_", "world"]);
16998 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16999 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17000 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17001 assert_eq!(split("helloworld"), &["helloworld"]);
17002
17003 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17004}
17005
17006#[gpui::test]
17007async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17008 init_test(cx, |_| {});
17009
17010 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17011 let mut assert = |before, after| {
17012 let _state_context = cx.set_state(before);
17013 cx.run_until_parked();
17014 cx.update_editor(|editor, window, cx| {
17015 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17016 });
17017 cx.run_until_parked();
17018 cx.assert_editor_state(after);
17019 };
17020
17021 // Outside bracket jumps to outside of matching bracket
17022 assert("console.logˇ(var);", "console.log(var)ˇ;");
17023 assert("console.log(var)ˇ;", "console.logˇ(var);");
17024
17025 // Inside bracket jumps to inside of matching bracket
17026 assert("console.log(ˇvar);", "console.log(varˇ);");
17027 assert("console.log(varˇ);", "console.log(ˇvar);");
17028
17029 // When outside a bracket and inside, favor jumping to the inside bracket
17030 assert(
17031 "console.log('foo', [1, 2, 3]ˇ);",
17032 "console.log(ˇ'foo', [1, 2, 3]);",
17033 );
17034 assert(
17035 "console.log(ˇ'foo', [1, 2, 3]);",
17036 "console.log('foo', [1, 2, 3]ˇ);",
17037 );
17038
17039 // Bias forward if two options are equally likely
17040 assert(
17041 "let result = curried_fun()ˇ();",
17042 "let result = curried_fun()()ˇ;",
17043 );
17044
17045 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17046 assert(
17047 indoc! {"
17048 function test() {
17049 console.log('test')ˇ
17050 }"},
17051 indoc! {"
17052 function test() {
17053 console.logˇ('test')
17054 }"},
17055 );
17056}
17057
17058#[gpui::test]
17059async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17060 init_test(cx, |_| {});
17061
17062 let fs = FakeFs::new(cx.executor());
17063 fs.insert_tree(
17064 path!("/a"),
17065 json!({
17066 "main.rs": "fn main() { let a = 5; }",
17067 "other.rs": "// Test file",
17068 }),
17069 )
17070 .await;
17071 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17072
17073 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17074 language_registry.add(Arc::new(Language::new(
17075 LanguageConfig {
17076 name: "Rust".into(),
17077 matcher: LanguageMatcher {
17078 path_suffixes: vec!["rs".to_string()],
17079 ..Default::default()
17080 },
17081 brackets: BracketPairConfig {
17082 pairs: vec![BracketPair {
17083 start: "{".to_string(),
17084 end: "}".to_string(),
17085 close: true,
17086 surround: true,
17087 newline: true,
17088 }],
17089 disabled_scopes_by_bracket_ix: Vec::new(),
17090 },
17091 ..Default::default()
17092 },
17093 Some(tree_sitter_rust::LANGUAGE.into()),
17094 )));
17095 let mut fake_servers = language_registry.register_fake_lsp(
17096 "Rust",
17097 FakeLspAdapter {
17098 capabilities: lsp::ServerCapabilities {
17099 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17100 first_trigger_character: "{".to_string(),
17101 more_trigger_character: None,
17102 }),
17103 ..Default::default()
17104 },
17105 ..Default::default()
17106 },
17107 );
17108
17109 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17110
17111 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17112
17113 let worktree_id = workspace
17114 .update(cx, |workspace, _, cx| {
17115 workspace.project().update(cx, |project, cx| {
17116 project.worktrees(cx).next().unwrap().read(cx).id()
17117 })
17118 })
17119 .unwrap();
17120
17121 let buffer = project
17122 .update(cx, |project, cx| {
17123 project.open_local_buffer(path!("/a/main.rs"), cx)
17124 })
17125 .await
17126 .unwrap();
17127 let editor_handle = workspace
17128 .update(cx, |workspace, window, cx| {
17129 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17130 })
17131 .unwrap()
17132 .await
17133 .unwrap()
17134 .downcast::<Editor>()
17135 .unwrap();
17136
17137 cx.executor().start_waiting();
17138 let fake_server = fake_servers.next().await.unwrap();
17139
17140 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17141 |params, _| async move {
17142 assert_eq!(
17143 params.text_document_position.text_document.uri,
17144 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17145 );
17146 assert_eq!(
17147 params.text_document_position.position,
17148 lsp::Position::new(0, 21),
17149 );
17150
17151 Ok(Some(vec![lsp::TextEdit {
17152 new_text: "]".to_string(),
17153 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17154 }]))
17155 },
17156 );
17157
17158 editor_handle.update_in(cx, |editor, window, cx| {
17159 window.focus(&editor.focus_handle(cx));
17160 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17161 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17162 });
17163 editor.handle_input("{", window, cx);
17164 });
17165
17166 cx.executor().run_until_parked();
17167
17168 buffer.update(cx, |buffer, _| {
17169 assert_eq!(
17170 buffer.text(),
17171 "fn main() { let a = {5}; }",
17172 "No extra braces from on type formatting should appear in the buffer"
17173 )
17174 });
17175}
17176
17177#[gpui::test(iterations = 20, seeds(31))]
17178async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17179 init_test(cx, |_| {});
17180
17181 let mut cx = EditorLspTestContext::new_rust(
17182 lsp::ServerCapabilities {
17183 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17184 first_trigger_character: ".".to_string(),
17185 more_trigger_character: None,
17186 }),
17187 ..Default::default()
17188 },
17189 cx,
17190 )
17191 .await;
17192
17193 cx.update_buffer(|buffer, _| {
17194 // This causes autoindent to be async.
17195 buffer.set_sync_parse_timeout(Duration::ZERO)
17196 });
17197
17198 cx.set_state("fn c() {\n d()ˇ\n}\n");
17199 cx.simulate_keystroke("\n");
17200 cx.run_until_parked();
17201
17202 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17203 let mut request =
17204 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17205 let buffer_cloned = buffer_cloned.clone();
17206 async move {
17207 buffer_cloned.update(&mut cx, |buffer, _| {
17208 assert_eq!(
17209 buffer.text(),
17210 "fn c() {\n d()\n .\n}\n",
17211 "OnTypeFormatting should triggered after autoindent applied"
17212 )
17213 })?;
17214
17215 Ok(Some(vec![]))
17216 }
17217 });
17218
17219 cx.simulate_keystroke(".");
17220 cx.run_until_parked();
17221
17222 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17223 assert!(request.next().await.is_some());
17224 request.close();
17225 assert!(request.next().await.is_none());
17226}
17227
17228#[gpui::test]
17229async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17230 init_test(cx, |_| {});
17231
17232 let fs = FakeFs::new(cx.executor());
17233 fs.insert_tree(
17234 path!("/a"),
17235 json!({
17236 "main.rs": "fn main() { let a = 5; }",
17237 "other.rs": "// Test file",
17238 }),
17239 )
17240 .await;
17241
17242 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17243
17244 let server_restarts = Arc::new(AtomicUsize::new(0));
17245 let closure_restarts = Arc::clone(&server_restarts);
17246 let language_server_name = "test language server";
17247 let language_name: LanguageName = "Rust".into();
17248
17249 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17250 language_registry.add(Arc::new(Language::new(
17251 LanguageConfig {
17252 name: language_name.clone(),
17253 matcher: LanguageMatcher {
17254 path_suffixes: vec!["rs".to_string()],
17255 ..Default::default()
17256 },
17257 ..Default::default()
17258 },
17259 Some(tree_sitter_rust::LANGUAGE.into()),
17260 )));
17261 let mut fake_servers = language_registry.register_fake_lsp(
17262 "Rust",
17263 FakeLspAdapter {
17264 name: language_server_name,
17265 initialization_options: Some(json!({
17266 "testOptionValue": true
17267 })),
17268 initializer: Some(Box::new(move |fake_server| {
17269 let task_restarts = Arc::clone(&closure_restarts);
17270 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17271 task_restarts.fetch_add(1, atomic::Ordering::Release);
17272 futures::future::ready(Ok(()))
17273 });
17274 })),
17275 ..Default::default()
17276 },
17277 );
17278
17279 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17280 let _buffer = project
17281 .update(cx, |project, cx| {
17282 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17283 })
17284 .await
17285 .unwrap();
17286 let _fake_server = fake_servers.next().await.unwrap();
17287 update_test_language_settings(cx, |language_settings| {
17288 language_settings.languages.0.insert(
17289 language_name.clone().0,
17290 LanguageSettingsContent {
17291 tab_size: NonZeroU32::new(8),
17292 ..Default::default()
17293 },
17294 );
17295 });
17296 cx.executor().run_until_parked();
17297 assert_eq!(
17298 server_restarts.load(atomic::Ordering::Acquire),
17299 0,
17300 "Should not restart LSP server on an unrelated change"
17301 );
17302
17303 update_test_project_settings(cx, |project_settings| {
17304 project_settings.lsp.insert(
17305 "Some other server name".into(),
17306 LspSettings {
17307 binary: None,
17308 settings: None,
17309 initialization_options: Some(json!({
17310 "some other init value": false
17311 })),
17312 enable_lsp_tasks: false,
17313 fetch: None,
17314 },
17315 );
17316 });
17317 cx.executor().run_until_parked();
17318 assert_eq!(
17319 server_restarts.load(atomic::Ordering::Acquire),
17320 0,
17321 "Should not restart LSP server on an unrelated LSP settings change"
17322 );
17323
17324 update_test_project_settings(cx, |project_settings| {
17325 project_settings.lsp.insert(
17326 language_server_name.into(),
17327 LspSettings {
17328 binary: None,
17329 settings: None,
17330 initialization_options: Some(json!({
17331 "anotherInitValue": false
17332 })),
17333 enable_lsp_tasks: false,
17334 fetch: None,
17335 },
17336 );
17337 });
17338 cx.executor().run_until_parked();
17339 assert_eq!(
17340 server_restarts.load(atomic::Ordering::Acquire),
17341 1,
17342 "Should restart LSP server on a related LSP settings change"
17343 );
17344
17345 update_test_project_settings(cx, |project_settings| {
17346 project_settings.lsp.insert(
17347 language_server_name.into(),
17348 LspSettings {
17349 binary: None,
17350 settings: None,
17351 initialization_options: Some(json!({
17352 "anotherInitValue": false
17353 })),
17354 enable_lsp_tasks: false,
17355 fetch: None,
17356 },
17357 );
17358 });
17359 cx.executor().run_until_parked();
17360 assert_eq!(
17361 server_restarts.load(atomic::Ordering::Acquire),
17362 1,
17363 "Should not restart LSP server on a related LSP settings change that is the same"
17364 );
17365
17366 update_test_project_settings(cx, |project_settings| {
17367 project_settings.lsp.insert(
17368 language_server_name.into(),
17369 LspSettings {
17370 binary: None,
17371 settings: None,
17372 initialization_options: None,
17373 enable_lsp_tasks: false,
17374 fetch: None,
17375 },
17376 );
17377 });
17378 cx.executor().run_until_parked();
17379 assert_eq!(
17380 server_restarts.load(atomic::Ordering::Acquire),
17381 2,
17382 "Should restart LSP server on another related LSP settings change"
17383 );
17384}
17385
17386#[gpui::test]
17387async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17388 init_test(cx, |_| {});
17389
17390 let mut cx = EditorLspTestContext::new_rust(
17391 lsp::ServerCapabilities {
17392 completion_provider: Some(lsp::CompletionOptions {
17393 trigger_characters: Some(vec![".".to_string()]),
17394 resolve_provider: Some(true),
17395 ..Default::default()
17396 }),
17397 ..Default::default()
17398 },
17399 cx,
17400 )
17401 .await;
17402
17403 cx.set_state("fn main() { let a = 2ˇ; }");
17404 cx.simulate_keystroke(".");
17405 let completion_item = lsp::CompletionItem {
17406 label: "some".into(),
17407 kind: Some(lsp::CompletionItemKind::SNIPPET),
17408 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17409 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17410 kind: lsp::MarkupKind::Markdown,
17411 value: "```rust\nSome(2)\n```".to_string(),
17412 })),
17413 deprecated: Some(false),
17414 sort_text: Some("fffffff2".to_string()),
17415 filter_text: Some("some".to_string()),
17416 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17417 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17418 range: lsp::Range {
17419 start: lsp::Position {
17420 line: 0,
17421 character: 22,
17422 },
17423 end: lsp::Position {
17424 line: 0,
17425 character: 22,
17426 },
17427 },
17428 new_text: "Some(2)".to_string(),
17429 })),
17430 additional_text_edits: Some(vec![lsp::TextEdit {
17431 range: lsp::Range {
17432 start: lsp::Position {
17433 line: 0,
17434 character: 20,
17435 },
17436 end: lsp::Position {
17437 line: 0,
17438 character: 22,
17439 },
17440 },
17441 new_text: "".to_string(),
17442 }]),
17443 ..Default::default()
17444 };
17445
17446 let closure_completion_item = completion_item.clone();
17447 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17448 let task_completion_item = closure_completion_item.clone();
17449 async move {
17450 Ok(Some(lsp::CompletionResponse::Array(vec![
17451 task_completion_item,
17452 ])))
17453 }
17454 });
17455
17456 request.next().await;
17457
17458 cx.condition(|editor, _| editor.context_menu_visible())
17459 .await;
17460 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17461 editor
17462 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17463 .unwrap()
17464 });
17465 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17466
17467 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17468 let task_completion_item = completion_item.clone();
17469 async move { Ok(task_completion_item) }
17470 })
17471 .next()
17472 .await
17473 .unwrap();
17474 apply_additional_edits.await.unwrap();
17475 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17476}
17477
17478#[gpui::test]
17479async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17480 init_test(cx, |_| {});
17481
17482 let mut cx = EditorLspTestContext::new_rust(
17483 lsp::ServerCapabilities {
17484 completion_provider: Some(lsp::CompletionOptions {
17485 trigger_characters: Some(vec![".".to_string()]),
17486 resolve_provider: Some(true),
17487 ..Default::default()
17488 }),
17489 ..Default::default()
17490 },
17491 cx,
17492 )
17493 .await;
17494
17495 cx.set_state("fn main() { let a = 2ˇ; }");
17496 cx.simulate_keystroke(".");
17497
17498 let item1 = lsp::CompletionItem {
17499 label: "method id()".to_string(),
17500 filter_text: Some("id".to_string()),
17501 detail: None,
17502 documentation: None,
17503 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17504 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17505 new_text: ".id".to_string(),
17506 })),
17507 ..lsp::CompletionItem::default()
17508 };
17509
17510 let item2 = lsp::CompletionItem {
17511 label: "other".to_string(),
17512 filter_text: Some("other".to_string()),
17513 detail: None,
17514 documentation: None,
17515 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17516 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17517 new_text: ".other".to_string(),
17518 })),
17519 ..lsp::CompletionItem::default()
17520 };
17521
17522 let item1 = item1.clone();
17523 cx.set_request_handler::<lsp::request::Completion, _, _>({
17524 let item1 = item1.clone();
17525 move |_, _, _| {
17526 let item1 = item1.clone();
17527 let item2 = item2.clone();
17528 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17529 }
17530 })
17531 .next()
17532 .await;
17533
17534 cx.condition(|editor, _| editor.context_menu_visible())
17535 .await;
17536 cx.update_editor(|editor, _, _| {
17537 let context_menu = editor.context_menu.borrow_mut();
17538 let context_menu = context_menu
17539 .as_ref()
17540 .expect("Should have the context menu deployed");
17541 match context_menu {
17542 CodeContextMenu::Completions(completions_menu) => {
17543 let completions = completions_menu.completions.borrow_mut();
17544 assert_eq!(
17545 completions
17546 .iter()
17547 .map(|completion| &completion.label.text)
17548 .collect::<Vec<_>>(),
17549 vec!["method id()", "other"]
17550 )
17551 }
17552 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17553 }
17554 });
17555
17556 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17557 let item1 = item1.clone();
17558 move |_, item_to_resolve, _| {
17559 let item1 = item1.clone();
17560 async move {
17561 if item1 == item_to_resolve {
17562 Ok(lsp::CompletionItem {
17563 label: "method id()".to_string(),
17564 filter_text: Some("id".to_string()),
17565 detail: Some("Now resolved!".to_string()),
17566 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17567 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17568 range: lsp::Range::new(
17569 lsp::Position::new(0, 22),
17570 lsp::Position::new(0, 22),
17571 ),
17572 new_text: ".id".to_string(),
17573 })),
17574 ..lsp::CompletionItem::default()
17575 })
17576 } else {
17577 Ok(item_to_resolve)
17578 }
17579 }
17580 }
17581 })
17582 .next()
17583 .await
17584 .unwrap();
17585 cx.run_until_parked();
17586
17587 cx.update_editor(|editor, window, cx| {
17588 editor.context_menu_next(&Default::default(), window, cx);
17589 });
17590
17591 cx.update_editor(|editor, _, _| {
17592 let context_menu = editor.context_menu.borrow_mut();
17593 let context_menu = context_menu
17594 .as_ref()
17595 .expect("Should have the context menu deployed");
17596 match context_menu {
17597 CodeContextMenu::Completions(completions_menu) => {
17598 let completions = completions_menu.completions.borrow_mut();
17599 assert_eq!(
17600 completions
17601 .iter()
17602 .map(|completion| &completion.label.text)
17603 .collect::<Vec<_>>(),
17604 vec!["method id() Now resolved!", "other"],
17605 "Should update first completion label, but not second as the filter text did not match."
17606 );
17607 }
17608 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17609 }
17610 });
17611}
17612
17613#[gpui::test]
17614async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17615 init_test(cx, |_| {});
17616 let mut cx = EditorLspTestContext::new_rust(
17617 lsp::ServerCapabilities {
17618 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17619 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17620 completion_provider: Some(lsp::CompletionOptions {
17621 resolve_provider: Some(true),
17622 ..Default::default()
17623 }),
17624 ..Default::default()
17625 },
17626 cx,
17627 )
17628 .await;
17629 cx.set_state(indoc! {"
17630 struct TestStruct {
17631 field: i32
17632 }
17633
17634 fn mainˇ() {
17635 let unused_var = 42;
17636 let test_struct = TestStruct { field: 42 };
17637 }
17638 "});
17639 let symbol_range = cx.lsp_range(indoc! {"
17640 struct TestStruct {
17641 field: i32
17642 }
17643
17644 «fn main»() {
17645 let unused_var = 42;
17646 let test_struct = TestStruct { field: 42 };
17647 }
17648 "});
17649 let mut hover_requests =
17650 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17651 Ok(Some(lsp::Hover {
17652 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17653 kind: lsp::MarkupKind::Markdown,
17654 value: "Function documentation".to_string(),
17655 }),
17656 range: Some(symbol_range),
17657 }))
17658 });
17659
17660 // Case 1: Test that code action menu hide hover popover
17661 cx.dispatch_action(Hover);
17662 hover_requests.next().await;
17663 cx.condition(|editor, _| editor.hover_state.visible()).await;
17664 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17665 move |_, _, _| async move {
17666 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17667 lsp::CodeAction {
17668 title: "Remove unused variable".to_string(),
17669 kind: Some(CodeActionKind::QUICKFIX),
17670 edit: Some(lsp::WorkspaceEdit {
17671 changes: Some(
17672 [(
17673 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17674 vec![lsp::TextEdit {
17675 range: lsp::Range::new(
17676 lsp::Position::new(5, 4),
17677 lsp::Position::new(5, 27),
17678 ),
17679 new_text: "".to_string(),
17680 }],
17681 )]
17682 .into_iter()
17683 .collect(),
17684 ),
17685 ..Default::default()
17686 }),
17687 ..Default::default()
17688 },
17689 )]))
17690 },
17691 );
17692 cx.update_editor(|editor, window, cx| {
17693 editor.toggle_code_actions(
17694 &ToggleCodeActions {
17695 deployed_from: None,
17696 quick_launch: false,
17697 },
17698 window,
17699 cx,
17700 );
17701 });
17702 code_action_requests.next().await;
17703 cx.run_until_parked();
17704 cx.condition(|editor, _| editor.context_menu_visible())
17705 .await;
17706 cx.update_editor(|editor, _, _| {
17707 assert!(
17708 !editor.hover_state.visible(),
17709 "Hover popover should be hidden when code action menu is shown"
17710 );
17711 // Hide code actions
17712 editor.context_menu.take();
17713 });
17714
17715 // Case 2: Test that code completions hide hover popover
17716 cx.dispatch_action(Hover);
17717 hover_requests.next().await;
17718 cx.condition(|editor, _| editor.hover_state.visible()).await;
17719 let counter = Arc::new(AtomicUsize::new(0));
17720 let mut completion_requests =
17721 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17722 let counter = counter.clone();
17723 async move {
17724 counter.fetch_add(1, atomic::Ordering::Release);
17725 Ok(Some(lsp::CompletionResponse::Array(vec![
17726 lsp::CompletionItem {
17727 label: "main".into(),
17728 kind: Some(lsp::CompletionItemKind::FUNCTION),
17729 detail: Some("() -> ()".to_string()),
17730 ..Default::default()
17731 },
17732 lsp::CompletionItem {
17733 label: "TestStruct".into(),
17734 kind: Some(lsp::CompletionItemKind::STRUCT),
17735 detail: Some("struct TestStruct".to_string()),
17736 ..Default::default()
17737 },
17738 ])))
17739 }
17740 });
17741 cx.update_editor(|editor, window, cx| {
17742 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17743 });
17744 completion_requests.next().await;
17745 cx.condition(|editor, _| editor.context_menu_visible())
17746 .await;
17747 cx.update_editor(|editor, _, _| {
17748 assert!(
17749 !editor.hover_state.visible(),
17750 "Hover popover should be hidden when completion menu is shown"
17751 );
17752 });
17753}
17754
17755#[gpui::test]
17756async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17757 init_test(cx, |_| {});
17758
17759 let mut cx = EditorLspTestContext::new_rust(
17760 lsp::ServerCapabilities {
17761 completion_provider: Some(lsp::CompletionOptions {
17762 trigger_characters: Some(vec![".".to_string()]),
17763 resolve_provider: Some(true),
17764 ..Default::default()
17765 }),
17766 ..Default::default()
17767 },
17768 cx,
17769 )
17770 .await;
17771
17772 cx.set_state("fn main() { let a = 2ˇ; }");
17773 cx.simulate_keystroke(".");
17774
17775 let unresolved_item_1 = lsp::CompletionItem {
17776 label: "id".to_string(),
17777 filter_text: Some("id".to_string()),
17778 detail: None,
17779 documentation: None,
17780 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17781 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17782 new_text: ".id".to_string(),
17783 })),
17784 ..lsp::CompletionItem::default()
17785 };
17786 let resolved_item_1 = lsp::CompletionItem {
17787 additional_text_edits: Some(vec![lsp::TextEdit {
17788 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17789 new_text: "!!".to_string(),
17790 }]),
17791 ..unresolved_item_1.clone()
17792 };
17793 let unresolved_item_2 = lsp::CompletionItem {
17794 label: "other".to_string(),
17795 filter_text: Some("other".to_string()),
17796 detail: None,
17797 documentation: None,
17798 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17799 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17800 new_text: ".other".to_string(),
17801 })),
17802 ..lsp::CompletionItem::default()
17803 };
17804 let resolved_item_2 = lsp::CompletionItem {
17805 additional_text_edits: Some(vec![lsp::TextEdit {
17806 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17807 new_text: "??".to_string(),
17808 }]),
17809 ..unresolved_item_2.clone()
17810 };
17811
17812 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17813 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17814 cx.lsp
17815 .server
17816 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17817 let unresolved_item_1 = unresolved_item_1.clone();
17818 let resolved_item_1 = resolved_item_1.clone();
17819 let unresolved_item_2 = unresolved_item_2.clone();
17820 let resolved_item_2 = resolved_item_2.clone();
17821 let resolve_requests_1 = resolve_requests_1.clone();
17822 let resolve_requests_2 = resolve_requests_2.clone();
17823 move |unresolved_request, _| {
17824 let unresolved_item_1 = unresolved_item_1.clone();
17825 let resolved_item_1 = resolved_item_1.clone();
17826 let unresolved_item_2 = unresolved_item_2.clone();
17827 let resolved_item_2 = resolved_item_2.clone();
17828 let resolve_requests_1 = resolve_requests_1.clone();
17829 let resolve_requests_2 = resolve_requests_2.clone();
17830 async move {
17831 if unresolved_request == unresolved_item_1 {
17832 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17833 Ok(resolved_item_1.clone())
17834 } else if unresolved_request == unresolved_item_2 {
17835 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17836 Ok(resolved_item_2.clone())
17837 } else {
17838 panic!("Unexpected completion item {unresolved_request:?}")
17839 }
17840 }
17841 }
17842 })
17843 .detach();
17844
17845 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17846 let unresolved_item_1 = unresolved_item_1.clone();
17847 let unresolved_item_2 = unresolved_item_2.clone();
17848 async move {
17849 Ok(Some(lsp::CompletionResponse::Array(vec![
17850 unresolved_item_1,
17851 unresolved_item_2,
17852 ])))
17853 }
17854 })
17855 .next()
17856 .await;
17857
17858 cx.condition(|editor, _| editor.context_menu_visible())
17859 .await;
17860 cx.update_editor(|editor, _, _| {
17861 let context_menu = editor.context_menu.borrow_mut();
17862 let context_menu = context_menu
17863 .as_ref()
17864 .expect("Should have the context menu deployed");
17865 match context_menu {
17866 CodeContextMenu::Completions(completions_menu) => {
17867 let completions = completions_menu.completions.borrow_mut();
17868 assert_eq!(
17869 completions
17870 .iter()
17871 .map(|completion| &completion.label.text)
17872 .collect::<Vec<_>>(),
17873 vec!["id", "other"]
17874 )
17875 }
17876 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17877 }
17878 });
17879 cx.run_until_parked();
17880
17881 cx.update_editor(|editor, window, cx| {
17882 editor.context_menu_next(&ContextMenuNext, window, cx);
17883 });
17884 cx.run_until_parked();
17885 cx.update_editor(|editor, window, cx| {
17886 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17887 });
17888 cx.run_until_parked();
17889 cx.update_editor(|editor, window, cx| {
17890 editor.context_menu_next(&ContextMenuNext, window, cx);
17891 });
17892 cx.run_until_parked();
17893 cx.update_editor(|editor, window, cx| {
17894 editor
17895 .compose_completion(&ComposeCompletion::default(), window, cx)
17896 .expect("No task returned")
17897 })
17898 .await
17899 .expect("Completion failed");
17900 cx.run_until_parked();
17901
17902 cx.update_editor(|editor, _, cx| {
17903 assert_eq!(
17904 resolve_requests_1.load(atomic::Ordering::Acquire),
17905 1,
17906 "Should always resolve once despite multiple selections"
17907 );
17908 assert_eq!(
17909 resolve_requests_2.load(atomic::Ordering::Acquire),
17910 1,
17911 "Should always resolve once after multiple selections and applying the completion"
17912 );
17913 assert_eq!(
17914 editor.text(cx),
17915 "fn main() { let a = ??.other; }",
17916 "Should use resolved data when applying the completion"
17917 );
17918 });
17919}
17920
17921#[gpui::test]
17922async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17923 init_test(cx, |_| {});
17924
17925 let item_0 = lsp::CompletionItem {
17926 label: "abs".into(),
17927 insert_text: Some("abs".into()),
17928 data: Some(json!({ "very": "special"})),
17929 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17930 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17931 lsp::InsertReplaceEdit {
17932 new_text: "abs".to_string(),
17933 insert: lsp::Range::default(),
17934 replace: lsp::Range::default(),
17935 },
17936 )),
17937 ..lsp::CompletionItem::default()
17938 };
17939 let items = iter::once(item_0.clone())
17940 .chain((11..51).map(|i| lsp::CompletionItem {
17941 label: format!("item_{}", i),
17942 insert_text: Some(format!("item_{}", i)),
17943 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17944 ..lsp::CompletionItem::default()
17945 }))
17946 .collect::<Vec<_>>();
17947
17948 let default_commit_characters = vec!["?".to_string()];
17949 let default_data = json!({ "default": "data"});
17950 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17951 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17952 let default_edit_range = lsp::Range {
17953 start: lsp::Position {
17954 line: 0,
17955 character: 5,
17956 },
17957 end: lsp::Position {
17958 line: 0,
17959 character: 5,
17960 },
17961 };
17962
17963 let mut cx = EditorLspTestContext::new_rust(
17964 lsp::ServerCapabilities {
17965 completion_provider: Some(lsp::CompletionOptions {
17966 trigger_characters: Some(vec![".".to_string()]),
17967 resolve_provider: Some(true),
17968 ..Default::default()
17969 }),
17970 ..Default::default()
17971 },
17972 cx,
17973 )
17974 .await;
17975
17976 cx.set_state("fn main() { let a = 2ˇ; }");
17977 cx.simulate_keystroke(".");
17978
17979 let completion_data = default_data.clone();
17980 let completion_characters = default_commit_characters.clone();
17981 let completion_items = items.clone();
17982 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17983 let default_data = completion_data.clone();
17984 let default_commit_characters = completion_characters.clone();
17985 let items = completion_items.clone();
17986 async move {
17987 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17988 items,
17989 item_defaults: Some(lsp::CompletionListItemDefaults {
17990 data: Some(default_data.clone()),
17991 commit_characters: Some(default_commit_characters.clone()),
17992 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17993 default_edit_range,
17994 )),
17995 insert_text_format: Some(default_insert_text_format),
17996 insert_text_mode: Some(default_insert_text_mode),
17997 }),
17998 ..lsp::CompletionList::default()
17999 })))
18000 }
18001 })
18002 .next()
18003 .await;
18004
18005 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18006 cx.lsp
18007 .server
18008 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18009 let closure_resolved_items = resolved_items.clone();
18010 move |item_to_resolve, _| {
18011 let closure_resolved_items = closure_resolved_items.clone();
18012 async move {
18013 closure_resolved_items.lock().push(item_to_resolve.clone());
18014 Ok(item_to_resolve)
18015 }
18016 }
18017 })
18018 .detach();
18019
18020 cx.condition(|editor, _| editor.context_menu_visible())
18021 .await;
18022 cx.run_until_parked();
18023 cx.update_editor(|editor, _, _| {
18024 let menu = editor.context_menu.borrow_mut();
18025 match menu.as_ref().expect("should have the completions menu") {
18026 CodeContextMenu::Completions(completions_menu) => {
18027 assert_eq!(
18028 completions_menu
18029 .entries
18030 .borrow()
18031 .iter()
18032 .map(|mat| mat.string.clone())
18033 .collect::<Vec<String>>(),
18034 items
18035 .iter()
18036 .map(|completion| completion.label.clone())
18037 .collect::<Vec<String>>()
18038 );
18039 }
18040 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18041 }
18042 });
18043 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18044 // with 4 from the end.
18045 assert_eq!(
18046 *resolved_items.lock(),
18047 [&items[0..16], &items[items.len() - 4..items.len()]]
18048 .concat()
18049 .iter()
18050 .cloned()
18051 .map(|mut item| {
18052 if item.data.is_none() {
18053 item.data = Some(default_data.clone());
18054 }
18055 item
18056 })
18057 .collect::<Vec<lsp::CompletionItem>>(),
18058 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18059 );
18060 resolved_items.lock().clear();
18061
18062 cx.update_editor(|editor, window, cx| {
18063 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18064 });
18065 cx.run_until_parked();
18066 // Completions that have already been resolved are skipped.
18067 assert_eq!(
18068 *resolved_items.lock(),
18069 items[items.len() - 17..items.len() - 4]
18070 .iter()
18071 .cloned()
18072 .map(|mut item| {
18073 if item.data.is_none() {
18074 item.data = Some(default_data.clone());
18075 }
18076 item
18077 })
18078 .collect::<Vec<lsp::CompletionItem>>()
18079 );
18080 resolved_items.lock().clear();
18081}
18082
18083#[gpui::test]
18084async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18085 init_test(cx, |_| {});
18086
18087 let mut cx = EditorLspTestContext::new(
18088 Language::new(
18089 LanguageConfig {
18090 matcher: LanguageMatcher {
18091 path_suffixes: vec!["jsx".into()],
18092 ..Default::default()
18093 },
18094 overrides: [(
18095 "element".into(),
18096 LanguageConfigOverride {
18097 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18098 ..Default::default()
18099 },
18100 )]
18101 .into_iter()
18102 .collect(),
18103 ..Default::default()
18104 },
18105 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18106 )
18107 .with_override_query("(jsx_self_closing_element) @element")
18108 .unwrap(),
18109 lsp::ServerCapabilities {
18110 completion_provider: Some(lsp::CompletionOptions {
18111 trigger_characters: Some(vec![":".to_string()]),
18112 ..Default::default()
18113 }),
18114 ..Default::default()
18115 },
18116 cx,
18117 )
18118 .await;
18119
18120 cx.lsp
18121 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18122 Ok(Some(lsp::CompletionResponse::Array(vec![
18123 lsp::CompletionItem {
18124 label: "bg-blue".into(),
18125 ..Default::default()
18126 },
18127 lsp::CompletionItem {
18128 label: "bg-red".into(),
18129 ..Default::default()
18130 },
18131 lsp::CompletionItem {
18132 label: "bg-yellow".into(),
18133 ..Default::default()
18134 },
18135 ])))
18136 });
18137
18138 cx.set_state(r#"<p class="bgˇ" />"#);
18139
18140 // Trigger completion when typing a dash, because the dash is an extra
18141 // word character in the 'element' scope, which contains the cursor.
18142 cx.simulate_keystroke("-");
18143 cx.executor().run_until_parked();
18144 cx.update_editor(|editor, _, _| {
18145 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18146 {
18147 assert_eq!(
18148 completion_menu_entries(menu),
18149 &["bg-blue", "bg-red", "bg-yellow"]
18150 );
18151 } else {
18152 panic!("expected completion menu to be open");
18153 }
18154 });
18155
18156 cx.simulate_keystroke("l");
18157 cx.executor().run_until_parked();
18158 cx.update_editor(|editor, _, _| {
18159 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18160 {
18161 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18162 } else {
18163 panic!("expected completion menu to be open");
18164 }
18165 });
18166
18167 // When filtering completions, consider the character after the '-' to
18168 // be the start of a subword.
18169 cx.set_state(r#"<p class="yelˇ" />"#);
18170 cx.simulate_keystroke("l");
18171 cx.executor().run_until_parked();
18172 cx.update_editor(|editor, _, _| {
18173 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18174 {
18175 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18176 } else {
18177 panic!("expected completion menu to be open");
18178 }
18179 });
18180}
18181
18182fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18183 let entries = menu.entries.borrow();
18184 entries.iter().map(|mat| mat.string.clone()).collect()
18185}
18186
18187#[gpui::test]
18188async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18189 init_test(cx, |settings| {
18190 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
18191 Formatter::Prettier,
18192 )))
18193 });
18194
18195 let fs = FakeFs::new(cx.executor());
18196 fs.insert_file(path!("/file.ts"), Default::default()).await;
18197
18198 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18199 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18200
18201 language_registry.add(Arc::new(Language::new(
18202 LanguageConfig {
18203 name: "TypeScript".into(),
18204 matcher: LanguageMatcher {
18205 path_suffixes: vec!["ts".to_string()],
18206 ..Default::default()
18207 },
18208 ..Default::default()
18209 },
18210 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18211 )));
18212 update_test_language_settings(cx, |settings| {
18213 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18214 });
18215
18216 let test_plugin = "test_plugin";
18217 let _ = language_registry.register_fake_lsp(
18218 "TypeScript",
18219 FakeLspAdapter {
18220 prettier_plugins: vec![test_plugin],
18221 ..Default::default()
18222 },
18223 );
18224
18225 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18226 let buffer = project
18227 .update(cx, |project, cx| {
18228 project.open_local_buffer(path!("/file.ts"), cx)
18229 })
18230 .await
18231 .unwrap();
18232
18233 let buffer_text = "one\ntwo\nthree\n";
18234 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18235 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18236 editor.update_in(cx, |editor, window, cx| {
18237 editor.set_text(buffer_text, window, cx)
18238 });
18239
18240 editor
18241 .update_in(cx, |editor, window, cx| {
18242 editor.perform_format(
18243 project.clone(),
18244 FormatTrigger::Manual,
18245 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18246 window,
18247 cx,
18248 )
18249 })
18250 .unwrap()
18251 .await;
18252 assert_eq!(
18253 editor.update(cx, |editor, cx| editor.text(cx)),
18254 buffer_text.to_string() + prettier_format_suffix,
18255 "Test prettier formatting was not applied to the original buffer text",
18256 );
18257
18258 update_test_language_settings(cx, |settings| {
18259 settings.defaults.formatter = Some(SelectedFormatter::Auto)
18260 });
18261 let format = editor.update_in(cx, |editor, window, cx| {
18262 editor.perform_format(
18263 project.clone(),
18264 FormatTrigger::Manual,
18265 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18266 window,
18267 cx,
18268 )
18269 });
18270 format.await.unwrap();
18271 assert_eq!(
18272 editor.update(cx, |editor, cx| editor.text(cx)),
18273 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18274 "Autoformatting (via test prettier) was not applied to the original buffer text",
18275 );
18276}
18277
18278#[gpui::test]
18279async fn test_addition_reverts(cx: &mut TestAppContext) {
18280 init_test(cx, |_| {});
18281 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18282 let base_text = indoc! {r#"
18283 struct Row;
18284 struct Row1;
18285 struct Row2;
18286
18287 struct Row4;
18288 struct Row5;
18289 struct Row6;
18290
18291 struct Row8;
18292 struct Row9;
18293 struct Row10;"#};
18294
18295 // When addition hunks are not adjacent to carets, no hunk revert is performed
18296 assert_hunk_revert(
18297 indoc! {r#"struct Row;
18298 struct Row1;
18299 struct Row1.1;
18300 struct Row1.2;
18301 struct Row2;ˇ
18302
18303 struct Row4;
18304 struct Row5;
18305 struct Row6;
18306
18307 struct Row8;
18308 ˇstruct Row9;
18309 struct Row9.1;
18310 struct Row9.2;
18311 struct Row9.3;
18312 struct Row10;"#},
18313 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18314 indoc! {r#"struct Row;
18315 struct Row1;
18316 struct Row1.1;
18317 struct Row1.2;
18318 struct Row2;ˇ
18319
18320 struct Row4;
18321 struct Row5;
18322 struct Row6;
18323
18324 struct Row8;
18325 ˇstruct Row9;
18326 struct Row9.1;
18327 struct Row9.2;
18328 struct Row9.3;
18329 struct Row10;"#},
18330 base_text,
18331 &mut cx,
18332 );
18333 // Same for selections
18334 assert_hunk_revert(
18335 indoc! {r#"struct Row;
18336 struct Row1;
18337 struct Row2;
18338 struct Row2.1;
18339 struct Row2.2;
18340 «ˇ
18341 struct Row4;
18342 struct» Row5;
18343 «struct Row6;
18344 ˇ»
18345 struct Row9.1;
18346 struct Row9.2;
18347 struct Row9.3;
18348 struct Row8;
18349 struct Row9;
18350 struct Row10;"#},
18351 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18352 indoc! {r#"struct Row;
18353 struct Row1;
18354 struct Row2;
18355 struct Row2.1;
18356 struct Row2.2;
18357 «ˇ
18358 struct Row4;
18359 struct» Row5;
18360 «struct Row6;
18361 ˇ»
18362 struct Row9.1;
18363 struct Row9.2;
18364 struct Row9.3;
18365 struct Row8;
18366 struct Row9;
18367 struct Row10;"#},
18368 base_text,
18369 &mut cx,
18370 );
18371
18372 // When carets and selections intersect the addition hunks, those are reverted.
18373 // Adjacent carets got merged.
18374 assert_hunk_revert(
18375 indoc! {r#"struct Row;
18376 ˇ// something on the top
18377 struct Row1;
18378 struct Row2;
18379 struct Roˇw3.1;
18380 struct Row2.2;
18381 struct Row2.3;ˇ
18382
18383 struct Row4;
18384 struct ˇRow5.1;
18385 struct Row5.2;
18386 struct «Rowˇ»5.3;
18387 struct Row5;
18388 struct Row6;
18389 ˇ
18390 struct Row9.1;
18391 struct «Rowˇ»9.2;
18392 struct «ˇRow»9.3;
18393 struct Row8;
18394 struct Row9;
18395 «ˇ// something on bottom»
18396 struct Row10;"#},
18397 vec![
18398 DiffHunkStatusKind::Added,
18399 DiffHunkStatusKind::Added,
18400 DiffHunkStatusKind::Added,
18401 DiffHunkStatusKind::Added,
18402 DiffHunkStatusKind::Added,
18403 ],
18404 indoc! {r#"struct Row;
18405 ˇstruct Row1;
18406 struct Row2;
18407 ˇ
18408 struct Row4;
18409 ˇstruct Row5;
18410 struct Row6;
18411 ˇ
18412 ˇstruct Row8;
18413 struct Row9;
18414 ˇstruct Row10;"#},
18415 base_text,
18416 &mut cx,
18417 );
18418}
18419
18420#[gpui::test]
18421async fn test_modification_reverts(cx: &mut TestAppContext) {
18422 init_test(cx, |_| {});
18423 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18424 let base_text = indoc! {r#"
18425 struct Row;
18426 struct Row1;
18427 struct Row2;
18428
18429 struct Row4;
18430 struct Row5;
18431 struct Row6;
18432
18433 struct Row8;
18434 struct Row9;
18435 struct Row10;"#};
18436
18437 // Modification hunks behave the same as the addition ones.
18438 assert_hunk_revert(
18439 indoc! {r#"struct Row;
18440 struct Row1;
18441 struct Row33;
18442 ˇ
18443 struct Row4;
18444 struct Row5;
18445 struct Row6;
18446 ˇ
18447 struct Row99;
18448 struct Row9;
18449 struct Row10;"#},
18450 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18451 indoc! {r#"struct Row;
18452 struct Row1;
18453 struct Row33;
18454 ˇ
18455 struct Row4;
18456 struct Row5;
18457 struct Row6;
18458 ˇ
18459 struct Row99;
18460 struct Row9;
18461 struct Row10;"#},
18462 base_text,
18463 &mut cx,
18464 );
18465 assert_hunk_revert(
18466 indoc! {r#"struct Row;
18467 struct Row1;
18468 struct Row33;
18469 «ˇ
18470 struct Row4;
18471 struct» Row5;
18472 «struct Row6;
18473 ˇ»
18474 struct Row99;
18475 struct Row9;
18476 struct Row10;"#},
18477 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18478 indoc! {r#"struct Row;
18479 struct Row1;
18480 struct Row33;
18481 «ˇ
18482 struct Row4;
18483 struct» Row5;
18484 «struct Row6;
18485 ˇ»
18486 struct Row99;
18487 struct Row9;
18488 struct Row10;"#},
18489 base_text,
18490 &mut cx,
18491 );
18492
18493 assert_hunk_revert(
18494 indoc! {r#"ˇstruct Row1.1;
18495 struct Row1;
18496 «ˇstr»uct Row22;
18497
18498 struct ˇRow44;
18499 struct Row5;
18500 struct «Rˇ»ow66;ˇ
18501
18502 «struˇ»ct Row88;
18503 struct Row9;
18504 struct Row1011;ˇ"#},
18505 vec![
18506 DiffHunkStatusKind::Modified,
18507 DiffHunkStatusKind::Modified,
18508 DiffHunkStatusKind::Modified,
18509 DiffHunkStatusKind::Modified,
18510 DiffHunkStatusKind::Modified,
18511 DiffHunkStatusKind::Modified,
18512 ],
18513 indoc! {r#"struct Row;
18514 ˇstruct Row1;
18515 struct Row2;
18516 ˇ
18517 struct Row4;
18518 ˇstruct Row5;
18519 struct Row6;
18520 ˇ
18521 struct Row8;
18522 ˇstruct Row9;
18523 struct Row10;ˇ"#},
18524 base_text,
18525 &mut cx,
18526 );
18527}
18528
18529#[gpui::test]
18530async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18531 init_test(cx, |_| {});
18532 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18533 let base_text = indoc! {r#"
18534 one
18535
18536 two
18537 three
18538 "#};
18539
18540 cx.set_head_text(base_text);
18541 cx.set_state("\nˇ\n");
18542 cx.executor().run_until_parked();
18543 cx.update_editor(|editor, _window, cx| {
18544 editor.expand_selected_diff_hunks(cx);
18545 });
18546 cx.executor().run_until_parked();
18547 cx.update_editor(|editor, window, cx| {
18548 editor.backspace(&Default::default(), window, cx);
18549 });
18550 cx.run_until_parked();
18551 cx.assert_state_with_diff(
18552 indoc! {r#"
18553
18554 - two
18555 - threeˇ
18556 +
18557 "#}
18558 .to_string(),
18559 );
18560}
18561
18562#[gpui::test]
18563async fn test_deletion_reverts(cx: &mut TestAppContext) {
18564 init_test(cx, |_| {});
18565 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18566 let base_text = indoc! {r#"struct Row;
18567struct Row1;
18568struct Row2;
18569
18570struct Row4;
18571struct Row5;
18572struct Row6;
18573
18574struct Row8;
18575struct Row9;
18576struct Row10;"#};
18577
18578 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18579 assert_hunk_revert(
18580 indoc! {r#"struct Row;
18581 struct Row2;
18582
18583 ˇstruct Row4;
18584 struct Row5;
18585 struct Row6;
18586 ˇ
18587 struct Row8;
18588 struct Row10;"#},
18589 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18590 indoc! {r#"struct Row;
18591 struct Row2;
18592
18593 ˇstruct Row4;
18594 struct Row5;
18595 struct Row6;
18596 ˇ
18597 struct Row8;
18598 struct Row10;"#},
18599 base_text,
18600 &mut cx,
18601 );
18602 assert_hunk_revert(
18603 indoc! {r#"struct Row;
18604 struct Row2;
18605
18606 «ˇstruct Row4;
18607 struct» Row5;
18608 «struct Row6;
18609 ˇ»
18610 struct Row8;
18611 struct Row10;"#},
18612 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18613 indoc! {r#"struct Row;
18614 struct Row2;
18615
18616 «ˇstruct Row4;
18617 struct» Row5;
18618 «struct Row6;
18619 ˇ»
18620 struct Row8;
18621 struct Row10;"#},
18622 base_text,
18623 &mut cx,
18624 );
18625
18626 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18627 assert_hunk_revert(
18628 indoc! {r#"struct Row;
18629 ˇstruct Row2;
18630
18631 struct Row4;
18632 struct Row5;
18633 struct Row6;
18634
18635 struct Row8;ˇ
18636 struct Row10;"#},
18637 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18638 indoc! {r#"struct Row;
18639 struct Row1;
18640 ˇstruct Row2;
18641
18642 struct Row4;
18643 struct Row5;
18644 struct Row6;
18645
18646 struct Row8;ˇ
18647 struct Row9;
18648 struct Row10;"#},
18649 base_text,
18650 &mut cx,
18651 );
18652 assert_hunk_revert(
18653 indoc! {r#"struct Row;
18654 struct Row2«ˇ;
18655 struct Row4;
18656 struct» Row5;
18657 «struct Row6;
18658
18659 struct Row8;ˇ»
18660 struct Row10;"#},
18661 vec![
18662 DiffHunkStatusKind::Deleted,
18663 DiffHunkStatusKind::Deleted,
18664 DiffHunkStatusKind::Deleted,
18665 ],
18666 indoc! {r#"struct Row;
18667 struct Row1;
18668 struct Row2«ˇ;
18669
18670 struct Row4;
18671 struct» Row5;
18672 «struct Row6;
18673
18674 struct Row8;ˇ»
18675 struct Row9;
18676 struct Row10;"#},
18677 base_text,
18678 &mut cx,
18679 );
18680}
18681
18682#[gpui::test]
18683async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18684 init_test(cx, |_| {});
18685
18686 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18687 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18688 let base_text_3 =
18689 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18690
18691 let text_1 = edit_first_char_of_every_line(base_text_1);
18692 let text_2 = edit_first_char_of_every_line(base_text_2);
18693 let text_3 = edit_first_char_of_every_line(base_text_3);
18694
18695 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18696 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18697 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18698
18699 let multibuffer = cx.new(|cx| {
18700 let mut multibuffer = MultiBuffer::new(ReadWrite);
18701 multibuffer.push_excerpts(
18702 buffer_1.clone(),
18703 [
18704 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18705 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18706 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18707 ],
18708 cx,
18709 );
18710 multibuffer.push_excerpts(
18711 buffer_2.clone(),
18712 [
18713 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18714 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18715 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18716 ],
18717 cx,
18718 );
18719 multibuffer.push_excerpts(
18720 buffer_3.clone(),
18721 [
18722 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18723 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18724 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18725 ],
18726 cx,
18727 );
18728 multibuffer
18729 });
18730
18731 let fs = FakeFs::new(cx.executor());
18732 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18733 let (editor, cx) = cx
18734 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18735 editor.update_in(cx, |editor, _window, cx| {
18736 for (buffer, diff_base) in [
18737 (buffer_1.clone(), base_text_1),
18738 (buffer_2.clone(), base_text_2),
18739 (buffer_3.clone(), base_text_3),
18740 ] {
18741 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18742 editor
18743 .buffer
18744 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18745 }
18746 });
18747 cx.executor().run_until_parked();
18748
18749 editor.update_in(cx, |editor, window, cx| {
18750 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}");
18751 editor.select_all(&SelectAll, window, cx);
18752 editor.git_restore(&Default::default(), window, cx);
18753 });
18754 cx.executor().run_until_parked();
18755
18756 // When all ranges are selected, all buffer hunks are reverted.
18757 editor.update(cx, |editor, cx| {
18758 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");
18759 });
18760 buffer_1.update(cx, |buffer, _| {
18761 assert_eq!(buffer.text(), base_text_1);
18762 });
18763 buffer_2.update(cx, |buffer, _| {
18764 assert_eq!(buffer.text(), base_text_2);
18765 });
18766 buffer_3.update(cx, |buffer, _| {
18767 assert_eq!(buffer.text(), base_text_3);
18768 });
18769
18770 editor.update_in(cx, |editor, window, cx| {
18771 editor.undo(&Default::default(), window, cx);
18772 });
18773
18774 editor.update_in(cx, |editor, window, cx| {
18775 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18776 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18777 });
18778 editor.git_restore(&Default::default(), window, cx);
18779 });
18780
18781 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18782 // but not affect buffer_2 and its related excerpts.
18783 editor.update(cx, |editor, cx| {
18784 assert_eq!(
18785 editor.text(cx),
18786 "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}"
18787 );
18788 });
18789 buffer_1.update(cx, |buffer, _| {
18790 assert_eq!(buffer.text(), base_text_1);
18791 });
18792 buffer_2.update(cx, |buffer, _| {
18793 assert_eq!(
18794 buffer.text(),
18795 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18796 );
18797 });
18798 buffer_3.update(cx, |buffer, _| {
18799 assert_eq!(
18800 buffer.text(),
18801 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18802 );
18803 });
18804
18805 fn edit_first_char_of_every_line(text: &str) -> String {
18806 text.split('\n')
18807 .map(|line| format!("X{}", &line[1..]))
18808 .collect::<Vec<_>>()
18809 .join("\n")
18810 }
18811}
18812
18813#[gpui::test]
18814async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18815 init_test(cx, |_| {});
18816
18817 let cols = 4;
18818 let rows = 10;
18819 let sample_text_1 = sample_text(rows, cols, 'a');
18820 assert_eq!(
18821 sample_text_1,
18822 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18823 );
18824 let sample_text_2 = sample_text(rows, cols, 'l');
18825 assert_eq!(
18826 sample_text_2,
18827 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18828 );
18829 let sample_text_3 = sample_text(rows, cols, 'v');
18830 assert_eq!(
18831 sample_text_3,
18832 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18833 );
18834
18835 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18836 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18837 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18838
18839 let multi_buffer = cx.new(|cx| {
18840 let mut multibuffer = MultiBuffer::new(ReadWrite);
18841 multibuffer.push_excerpts(
18842 buffer_1.clone(),
18843 [
18844 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18845 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18846 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18847 ],
18848 cx,
18849 );
18850 multibuffer.push_excerpts(
18851 buffer_2.clone(),
18852 [
18853 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18854 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18855 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18856 ],
18857 cx,
18858 );
18859 multibuffer.push_excerpts(
18860 buffer_3.clone(),
18861 [
18862 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18863 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18864 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18865 ],
18866 cx,
18867 );
18868 multibuffer
18869 });
18870
18871 let fs = FakeFs::new(cx.executor());
18872 fs.insert_tree(
18873 "/a",
18874 json!({
18875 "main.rs": sample_text_1,
18876 "other.rs": sample_text_2,
18877 "lib.rs": sample_text_3,
18878 }),
18879 )
18880 .await;
18881 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18882 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18883 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18884 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18885 Editor::new(
18886 EditorMode::full(),
18887 multi_buffer,
18888 Some(project.clone()),
18889 window,
18890 cx,
18891 )
18892 });
18893 let multibuffer_item_id = workspace
18894 .update(cx, |workspace, window, cx| {
18895 assert!(
18896 workspace.active_item(cx).is_none(),
18897 "active item should be None before the first item is added"
18898 );
18899 workspace.add_item_to_active_pane(
18900 Box::new(multi_buffer_editor.clone()),
18901 None,
18902 true,
18903 window,
18904 cx,
18905 );
18906 let active_item = workspace
18907 .active_item(cx)
18908 .expect("should have an active item after adding the multi buffer");
18909 assert_eq!(
18910 active_item.buffer_kind(cx),
18911 ItemBufferKind::Multibuffer,
18912 "A multi buffer was expected to active after adding"
18913 );
18914 active_item.item_id()
18915 })
18916 .unwrap();
18917 cx.executor().run_until_parked();
18918
18919 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18920 editor.change_selections(
18921 SelectionEffects::scroll(Autoscroll::Next),
18922 window,
18923 cx,
18924 |s| s.select_ranges(Some(1..2)),
18925 );
18926 editor.open_excerpts(&OpenExcerpts, window, cx);
18927 });
18928 cx.executor().run_until_parked();
18929 let first_item_id = workspace
18930 .update(cx, |workspace, window, cx| {
18931 let active_item = workspace
18932 .active_item(cx)
18933 .expect("should have an active item after navigating into the 1st buffer");
18934 let first_item_id = active_item.item_id();
18935 assert_ne!(
18936 first_item_id, multibuffer_item_id,
18937 "Should navigate into the 1st buffer and activate it"
18938 );
18939 assert_eq!(
18940 active_item.buffer_kind(cx),
18941 ItemBufferKind::Singleton,
18942 "New active item should be a singleton buffer"
18943 );
18944 assert_eq!(
18945 active_item
18946 .act_as::<Editor>(cx)
18947 .expect("should have navigated into an editor for the 1st buffer")
18948 .read(cx)
18949 .text(cx),
18950 sample_text_1
18951 );
18952
18953 workspace
18954 .go_back(workspace.active_pane().downgrade(), window, cx)
18955 .detach_and_log_err(cx);
18956
18957 first_item_id
18958 })
18959 .unwrap();
18960 cx.executor().run_until_parked();
18961 workspace
18962 .update(cx, |workspace, _, cx| {
18963 let active_item = workspace
18964 .active_item(cx)
18965 .expect("should have an active item after navigating back");
18966 assert_eq!(
18967 active_item.item_id(),
18968 multibuffer_item_id,
18969 "Should navigate back to the multi buffer"
18970 );
18971 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
18972 })
18973 .unwrap();
18974
18975 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18976 editor.change_selections(
18977 SelectionEffects::scroll(Autoscroll::Next),
18978 window,
18979 cx,
18980 |s| s.select_ranges(Some(39..40)),
18981 );
18982 editor.open_excerpts(&OpenExcerpts, window, cx);
18983 });
18984 cx.executor().run_until_parked();
18985 let second_item_id = workspace
18986 .update(cx, |workspace, window, cx| {
18987 let active_item = workspace
18988 .active_item(cx)
18989 .expect("should have an active item after navigating into the 2nd buffer");
18990 let second_item_id = active_item.item_id();
18991 assert_ne!(
18992 second_item_id, multibuffer_item_id,
18993 "Should navigate away from the multibuffer"
18994 );
18995 assert_ne!(
18996 second_item_id, first_item_id,
18997 "Should navigate into the 2nd buffer and activate it"
18998 );
18999 assert_eq!(
19000 active_item.buffer_kind(cx),
19001 ItemBufferKind::Singleton,
19002 "New active item should be a singleton buffer"
19003 );
19004 assert_eq!(
19005 active_item
19006 .act_as::<Editor>(cx)
19007 .expect("should have navigated into an editor")
19008 .read(cx)
19009 .text(cx),
19010 sample_text_2
19011 );
19012
19013 workspace
19014 .go_back(workspace.active_pane().downgrade(), window, cx)
19015 .detach_and_log_err(cx);
19016
19017 second_item_id
19018 })
19019 .unwrap();
19020 cx.executor().run_until_parked();
19021 workspace
19022 .update(cx, |workspace, _, cx| {
19023 let active_item = workspace
19024 .active_item(cx)
19025 .expect("should have an active item after navigating back from the 2nd buffer");
19026 assert_eq!(
19027 active_item.item_id(),
19028 multibuffer_item_id,
19029 "Should navigate back from the 2nd buffer to the multi buffer"
19030 );
19031 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19032 })
19033 .unwrap();
19034
19035 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19036 editor.change_selections(
19037 SelectionEffects::scroll(Autoscroll::Next),
19038 window,
19039 cx,
19040 |s| s.select_ranges(Some(70..70)),
19041 );
19042 editor.open_excerpts(&OpenExcerpts, window, cx);
19043 });
19044 cx.executor().run_until_parked();
19045 workspace
19046 .update(cx, |workspace, window, cx| {
19047 let active_item = workspace
19048 .active_item(cx)
19049 .expect("should have an active item after navigating into the 3rd buffer");
19050 let third_item_id = active_item.item_id();
19051 assert_ne!(
19052 third_item_id, multibuffer_item_id,
19053 "Should navigate into the 3rd buffer and activate it"
19054 );
19055 assert_ne!(third_item_id, first_item_id);
19056 assert_ne!(third_item_id, second_item_id);
19057 assert_eq!(
19058 active_item.buffer_kind(cx),
19059 ItemBufferKind::Singleton,
19060 "New active item should be a singleton buffer"
19061 );
19062 assert_eq!(
19063 active_item
19064 .act_as::<Editor>(cx)
19065 .expect("should have navigated into an editor")
19066 .read(cx)
19067 .text(cx),
19068 sample_text_3
19069 );
19070
19071 workspace
19072 .go_back(workspace.active_pane().downgrade(), window, cx)
19073 .detach_and_log_err(cx);
19074 })
19075 .unwrap();
19076 cx.executor().run_until_parked();
19077 workspace
19078 .update(cx, |workspace, _, cx| {
19079 let active_item = workspace
19080 .active_item(cx)
19081 .expect("should have an active item after navigating back from the 3rd buffer");
19082 assert_eq!(
19083 active_item.item_id(),
19084 multibuffer_item_id,
19085 "Should navigate back from the 3rd buffer to the multi buffer"
19086 );
19087 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19088 })
19089 .unwrap();
19090}
19091
19092#[gpui::test]
19093async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19094 init_test(cx, |_| {});
19095
19096 let mut cx = EditorTestContext::new(cx).await;
19097
19098 let diff_base = r#"
19099 use some::mod;
19100
19101 const A: u32 = 42;
19102
19103 fn main() {
19104 println!("hello");
19105
19106 println!("world");
19107 }
19108 "#
19109 .unindent();
19110
19111 cx.set_state(
19112 &r#"
19113 use some::modified;
19114
19115 ˇ
19116 fn main() {
19117 println!("hello there");
19118
19119 println!("around the");
19120 println!("world");
19121 }
19122 "#
19123 .unindent(),
19124 );
19125
19126 cx.set_head_text(&diff_base);
19127 executor.run_until_parked();
19128
19129 cx.update_editor(|editor, window, cx| {
19130 editor.go_to_next_hunk(&GoToHunk, window, cx);
19131 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19132 });
19133 executor.run_until_parked();
19134 cx.assert_state_with_diff(
19135 r#"
19136 use some::modified;
19137
19138
19139 fn main() {
19140 - println!("hello");
19141 + ˇ println!("hello there");
19142
19143 println!("around the");
19144 println!("world");
19145 }
19146 "#
19147 .unindent(),
19148 );
19149
19150 cx.update_editor(|editor, window, cx| {
19151 for _ in 0..2 {
19152 editor.go_to_next_hunk(&GoToHunk, window, cx);
19153 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19154 }
19155 });
19156 executor.run_until_parked();
19157 cx.assert_state_with_diff(
19158 r#"
19159 - use some::mod;
19160 + ˇuse some::modified;
19161
19162
19163 fn main() {
19164 - println!("hello");
19165 + println!("hello there");
19166
19167 + println!("around the");
19168 println!("world");
19169 }
19170 "#
19171 .unindent(),
19172 );
19173
19174 cx.update_editor(|editor, window, cx| {
19175 editor.go_to_next_hunk(&GoToHunk, window, cx);
19176 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19177 });
19178 executor.run_until_parked();
19179 cx.assert_state_with_diff(
19180 r#"
19181 - use some::mod;
19182 + use some::modified;
19183
19184 - const A: u32 = 42;
19185 ˇ
19186 fn main() {
19187 - println!("hello");
19188 + println!("hello there");
19189
19190 + println!("around the");
19191 println!("world");
19192 }
19193 "#
19194 .unindent(),
19195 );
19196
19197 cx.update_editor(|editor, window, cx| {
19198 editor.cancel(&Cancel, window, cx);
19199 });
19200
19201 cx.assert_state_with_diff(
19202 r#"
19203 use some::modified;
19204
19205 ˇ
19206 fn main() {
19207 println!("hello there");
19208
19209 println!("around the");
19210 println!("world");
19211 }
19212 "#
19213 .unindent(),
19214 );
19215}
19216
19217#[gpui::test]
19218async fn test_diff_base_change_with_expanded_diff_hunks(
19219 executor: BackgroundExecutor,
19220 cx: &mut TestAppContext,
19221) {
19222 init_test(cx, |_| {});
19223
19224 let mut cx = EditorTestContext::new(cx).await;
19225
19226 let diff_base = r#"
19227 use some::mod1;
19228 use some::mod2;
19229
19230 const A: u32 = 42;
19231 const B: u32 = 42;
19232 const C: u32 = 42;
19233
19234 fn main() {
19235 println!("hello");
19236
19237 println!("world");
19238 }
19239 "#
19240 .unindent();
19241
19242 cx.set_state(
19243 &r#"
19244 use some::mod2;
19245
19246 const A: u32 = 42;
19247 const C: u32 = 42;
19248
19249 fn main(ˇ) {
19250 //println!("hello");
19251
19252 println!("world");
19253 //
19254 //
19255 }
19256 "#
19257 .unindent(),
19258 );
19259
19260 cx.set_head_text(&diff_base);
19261 executor.run_until_parked();
19262
19263 cx.update_editor(|editor, window, cx| {
19264 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19265 });
19266 executor.run_until_parked();
19267 cx.assert_state_with_diff(
19268 r#"
19269 - use some::mod1;
19270 use some::mod2;
19271
19272 const A: u32 = 42;
19273 - const B: u32 = 42;
19274 const C: u32 = 42;
19275
19276 fn main(ˇ) {
19277 - println!("hello");
19278 + //println!("hello");
19279
19280 println!("world");
19281 + //
19282 + //
19283 }
19284 "#
19285 .unindent(),
19286 );
19287
19288 cx.set_head_text("new diff base!");
19289 executor.run_until_parked();
19290 cx.assert_state_with_diff(
19291 r#"
19292 - new diff base!
19293 + use some::mod2;
19294 +
19295 + const A: u32 = 42;
19296 + const C: u32 = 42;
19297 +
19298 + fn main(ˇ) {
19299 + //println!("hello");
19300 +
19301 + println!("world");
19302 + //
19303 + //
19304 + }
19305 "#
19306 .unindent(),
19307 );
19308}
19309
19310#[gpui::test]
19311async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19312 init_test(cx, |_| {});
19313
19314 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19315 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19316 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19317 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19318 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19319 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19320
19321 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19322 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19323 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19324
19325 let multi_buffer = cx.new(|cx| {
19326 let mut multibuffer = MultiBuffer::new(ReadWrite);
19327 multibuffer.push_excerpts(
19328 buffer_1.clone(),
19329 [
19330 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19331 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19332 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19333 ],
19334 cx,
19335 );
19336 multibuffer.push_excerpts(
19337 buffer_2.clone(),
19338 [
19339 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19340 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19341 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19342 ],
19343 cx,
19344 );
19345 multibuffer.push_excerpts(
19346 buffer_3.clone(),
19347 [
19348 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19349 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19350 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19351 ],
19352 cx,
19353 );
19354 multibuffer
19355 });
19356
19357 let editor =
19358 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19359 editor
19360 .update(cx, |editor, _window, cx| {
19361 for (buffer, diff_base) in [
19362 (buffer_1.clone(), file_1_old),
19363 (buffer_2.clone(), file_2_old),
19364 (buffer_3.clone(), file_3_old),
19365 ] {
19366 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19367 editor
19368 .buffer
19369 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19370 }
19371 })
19372 .unwrap();
19373
19374 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19375 cx.run_until_parked();
19376
19377 cx.assert_editor_state(
19378 &"
19379 ˇaaa
19380 ccc
19381 ddd
19382
19383 ggg
19384 hhh
19385
19386
19387 lll
19388 mmm
19389 NNN
19390
19391 qqq
19392 rrr
19393
19394 uuu
19395 111
19396 222
19397 333
19398
19399 666
19400 777
19401
19402 000
19403 !!!"
19404 .unindent(),
19405 );
19406
19407 cx.update_editor(|editor, window, cx| {
19408 editor.select_all(&SelectAll, window, cx);
19409 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19410 });
19411 cx.executor().run_until_parked();
19412
19413 cx.assert_state_with_diff(
19414 "
19415 «aaa
19416 - bbb
19417 ccc
19418 ddd
19419
19420 ggg
19421 hhh
19422
19423
19424 lll
19425 mmm
19426 - nnn
19427 + NNN
19428
19429 qqq
19430 rrr
19431
19432 uuu
19433 111
19434 222
19435 333
19436
19437 + 666
19438 777
19439
19440 000
19441 !!!ˇ»"
19442 .unindent(),
19443 );
19444}
19445
19446#[gpui::test]
19447async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19448 init_test(cx, |_| {});
19449
19450 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19451 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19452
19453 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19454 let multi_buffer = cx.new(|cx| {
19455 let mut multibuffer = MultiBuffer::new(ReadWrite);
19456 multibuffer.push_excerpts(
19457 buffer.clone(),
19458 [
19459 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19460 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19461 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19462 ],
19463 cx,
19464 );
19465 multibuffer
19466 });
19467
19468 let editor =
19469 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19470 editor
19471 .update(cx, |editor, _window, cx| {
19472 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19473 editor
19474 .buffer
19475 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19476 })
19477 .unwrap();
19478
19479 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19480 cx.run_until_parked();
19481
19482 cx.update_editor(|editor, window, cx| {
19483 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19484 });
19485 cx.executor().run_until_parked();
19486
19487 // When the start of a hunk coincides with the start of its excerpt,
19488 // the hunk is expanded. When the start of a hunk is earlier than
19489 // the start of its excerpt, the hunk is not expanded.
19490 cx.assert_state_with_diff(
19491 "
19492 ˇaaa
19493 - bbb
19494 + BBB
19495
19496 - ddd
19497 - eee
19498 + DDD
19499 + EEE
19500 fff
19501
19502 iii
19503 "
19504 .unindent(),
19505 );
19506}
19507
19508#[gpui::test]
19509async fn test_edits_around_expanded_insertion_hunks(
19510 executor: BackgroundExecutor,
19511 cx: &mut TestAppContext,
19512) {
19513 init_test(cx, |_| {});
19514
19515 let mut cx = EditorTestContext::new(cx).await;
19516
19517 let diff_base = r#"
19518 use some::mod1;
19519 use some::mod2;
19520
19521 const A: u32 = 42;
19522
19523 fn main() {
19524 println!("hello");
19525
19526 println!("world");
19527 }
19528 "#
19529 .unindent();
19530 executor.run_until_parked();
19531 cx.set_state(
19532 &r#"
19533 use some::mod1;
19534 use some::mod2;
19535
19536 const A: u32 = 42;
19537 const B: u32 = 42;
19538 const C: u32 = 42;
19539 ˇ
19540
19541 fn main() {
19542 println!("hello");
19543
19544 println!("world");
19545 }
19546 "#
19547 .unindent(),
19548 );
19549
19550 cx.set_head_text(&diff_base);
19551 executor.run_until_parked();
19552
19553 cx.update_editor(|editor, window, cx| {
19554 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19555 });
19556 executor.run_until_parked();
19557
19558 cx.assert_state_with_diff(
19559 r#"
19560 use some::mod1;
19561 use some::mod2;
19562
19563 const A: u32 = 42;
19564 + const B: u32 = 42;
19565 + const C: u32 = 42;
19566 + ˇ
19567
19568 fn main() {
19569 println!("hello");
19570
19571 println!("world");
19572 }
19573 "#
19574 .unindent(),
19575 );
19576
19577 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19578 executor.run_until_parked();
19579
19580 cx.assert_state_with_diff(
19581 r#"
19582 use some::mod1;
19583 use some::mod2;
19584
19585 const A: u32 = 42;
19586 + const B: u32 = 42;
19587 + const C: u32 = 42;
19588 + const D: u32 = 42;
19589 + ˇ
19590
19591 fn main() {
19592 println!("hello");
19593
19594 println!("world");
19595 }
19596 "#
19597 .unindent(),
19598 );
19599
19600 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19601 executor.run_until_parked();
19602
19603 cx.assert_state_with_diff(
19604 r#"
19605 use some::mod1;
19606 use some::mod2;
19607
19608 const A: u32 = 42;
19609 + const B: u32 = 42;
19610 + const C: u32 = 42;
19611 + const D: u32 = 42;
19612 + const E: u32 = 42;
19613 + ˇ
19614
19615 fn main() {
19616 println!("hello");
19617
19618 println!("world");
19619 }
19620 "#
19621 .unindent(),
19622 );
19623
19624 cx.update_editor(|editor, window, cx| {
19625 editor.delete_line(&DeleteLine, window, cx);
19626 });
19627 executor.run_until_parked();
19628
19629 cx.assert_state_with_diff(
19630 r#"
19631 use some::mod1;
19632 use some::mod2;
19633
19634 const A: u32 = 42;
19635 + const B: u32 = 42;
19636 + const C: u32 = 42;
19637 + const D: u32 = 42;
19638 + const E: u32 = 42;
19639 ˇ
19640 fn main() {
19641 println!("hello");
19642
19643 println!("world");
19644 }
19645 "#
19646 .unindent(),
19647 );
19648
19649 cx.update_editor(|editor, window, cx| {
19650 editor.move_up(&MoveUp, window, cx);
19651 editor.delete_line(&DeleteLine, window, cx);
19652 editor.move_up(&MoveUp, window, cx);
19653 editor.delete_line(&DeleteLine, window, cx);
19654 editor.move_up(&MoveUp, window, cx);
19655 editor.delete_line(&DeleteLine, window, cx);
19656 });
19657 executor.run_until_parked();
19658 cx.assert_state_with_diff(
19659 r#"
19660 use some::mod1;
19661 use some::mod2;
19662
19663 const A: u32 = 42;
19664 + const B: u32 = 42;
19665 ˇ
19666 fn main() {
19667 println!("hello");
19668
19669 println!("world");
19670 }
19671 "#
19672 .unindent(),
19673 );
19674
19675 cx.update_editor(|editor, window, cx| {
19676 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19677 editor.delete_line(&DeleteLine, window, cx);
19678 });
19679 executor.run_until_parked();
19680 cx.assert_state_with_diff(
19681 r#"
19682 ˇ
19683 fn main() {
19684 println!("hello");
19685
19686 println!("world");
19687 }
19688 "#
19689 .unindent(),
19690 );
19691}
19692
19693#[gpui::test]
19694async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19695 init_test(cx, |_| {});
19696
19697 let mut cx = EditorTestContext::new(cx).await;
19698 cx.set_head_text(indoc! { "
19699 one
19700 two
19701 three
19702 four
19703 five
19704 "
19705 });
19706 cx.set_state(indoc! { "
19707 one
19708 ˇthree
19709 five
19710 "});
19711 cx.run_until_parked();
19712 cx.update_editor(|editor, window, cx| {
19713 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19714 });
19715 cx.assert_state_with_diff(
19716 indoc! { "
19717 one
19718 - two
19719 ˇthree
19720 - four
19721 five
19722 "}
19723 .to_string(),
19724 );
19725 cx.update_editor(|editor, window, cx| {
19726 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19727 });
19728
19729 cx.assert_state_with_diff(
19730 indoc! { "
19731 one
19732 ˇthree
19733 five
19734 "}
19735 .to_string(),
19736 );
19737
19738 cx.set_state(indoc! { "
19739 one
19740 ˇTWO
19741 three
19742 four
19743 five
19744 "});
19745 cx.run_until_parked();
19746 cx.update_editor(|editor, window, cx| {
19747 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19748 });
19749
19750 cx.assert_state_with_diff(
19751 indoc! { "
19752 one
19753 - two
19754 + ˇTWO
19755 three
19756 four
19757 five
19758 "}
19759 .to_string(),
19760 );
19761 cx.update_editor(|editor, window, cx| {
19762 editor.move_up(&Default::default(), window, cx);
19763 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19764 });
19765 cx.assert_state_with_diff(
19766 indoc! { "
19767 one
19768 ˇTWO
19769 three
19770 four
19771 five
19772 "}
19773 .to_string(),
19774 );
19775}
19776
19777#[gpui::test]
19778async fn test_edits_around_expanded_deletion_hunks(
19779 executor: BackgroundExecutor,
19780 cx: &mut TestAppContext,
19781) {
19782 init_test(cx, |_| {});
19783
19784 let mut cx = EditorTestContext::new(cx).await;
19785
19786 let diff_base = r#"
19787 use some::mod1;
19788 use some::mod2;
19789
19790 const A: u32 = 42;
19791 const B: u32 = 42;
19792 const C: u32 = 42;
19793
19794
19795 fn main() {
19796 println!("hello");
19797
19798 println!("world");
19799 }
19800 "#
19801 .unindent();
19802 executor.run_until_parked();
19803 cx.set_state(
19804 &r#"
19805 use some::mod1;
19806 use some::mod2;
19807
19808 ˇconst B: u32 = 42;
19809 const C: u32 = 42;
19810
19811
19812 fn main() {
19813 println!("hello");
19814
19815 println!("world");
19816 }
19817 "#
19818 .unindent(),
19819 );
19820
19821 cx.set_head_text(&diff_base);
19822 executor.run_until_parked();
19823
19824 cx.update_editor(|editor, window, cx| {
19825 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19826 });
19827 executor.run_until_parked();
19828
19829 cx.assert_state_with_diff(
19830 r#"
19831 use some::mod1;
19832 use some::mod2;
19833
19834 - const A: u32 = 42;
19835 ˇconst B: u32 = 42;
19836 const C: u32 = 42;
19837
19838
19839 fn main() {
19840 println!("hello");
19841
19842 println!("world");
19843 }
19844 "#
19845 .unindent(),
19846 );
19847
19848 cx.update_editor(|editor, window, cx| {
19849 editor.delete_line(&DeleteLine, window, cx);
19850 });
19851 executor.run_until_parked();
19852 cx.assert_state_with_diff(
19853 r#"
19854 use some::mod1;
19855 use some::mod2;
19856
19857 - const A: u32 = 42;
19858 - const B: u32 = 42;
19859 ˇconst C: u32 = 42;
19860
19861
19862 fn main() {
19863 println!("hello");
19864
19865 println!("world");
19866 }
19867 "#
19868 .unindent(),
19869 );
19870
19871 cx.update_editor(|editor, window, cx| {
19872 editor.delete_line(&DeleteLine, window, cx);
19873 });
19874 executor.run_until_parked();
19875 cx.assert_state_with_diff(
19876 r#"
19877 use some::mod1;
19878 use some::mod2;
19879
19880 - const A: u32 = 42;
19881 - const B: u32 = 42;
19882 - const C: u32 = 42;
19883 ˇ
19884
19885 fn main() {
19886 println!("hello");
19887
19888 println!("world");
19889 }
19890 "#
19891 .unindent(),
19892 );
19893
19894 cx.update_editor(|editor, window, cx| {
19895 editor.handle_input("replacement", window, cx);
19896 });
19897 executor.run_until_parked();
19898 cx.assert_state_with_diff(
19899 r#"
19900 use some::mod1;
19901 use some::mod2;
19902
19903 - const A: u32 = 42;
19904 - const B: u32 = 42;
19905 - const C: u32 = 42;
19906 -
19907 + replacementˇ
19908
19909 fn main() {
19910 println!("hello");
19911
19912 println!("world");
19913 }
19914 "#
19915 .unindent(),
19916 );
19917}
19918
19919#[gpui::test]
19920async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19921 init_test(cx, |_| {});
19922
19923 let mut cx = EditorTestContext::new(cx).await;
19924
19925 let base_text = r#"
19926 one
19927 two
19928 three
19929 four
19930 five
19931 "#
19932 .unindent();
19933 executor.run_until_parked();
19934 cx.set_state(
19935 &r#"
19936 one
19937 two
19938 fˇour
19939 five
19940 "#
19941 .unindent(),
19942 );
19943
19944 cx.set_head_text(&base_text);
19945 executor.run_until_parked();
19946
19947 cx.update_editor(|editor, window, cx| {
19948 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19949 });
19950 executor.run_until_parked();
19951
19952 cx.assert_state_with_diff(
19953 r#"
19954 one
19955 two
19956 - three
19957 fˇour
19958 five
19959 "#
19960 .unindent(),
19961 );
19962
19963 cx.update_editor(|editor, window, cx| {
19964 editor.backspace(&Backspace, window, cx);
19965 editor.backspace(&Backspace, window, cx);
19966 });
19967 executor.run_until_parked();
19968 cx.assert_state_with_diff(
19969 r#"
19970 one
19971 two
19972 - threeˇ
19973 - four
19974 + our
19975 five
19976 "#
19977 .unindent(),
19978 );
19979}
19980
19981#[gpui::test]
19982async fn test_edit_after_expanded_modification_hunk(
19983 executor: BackgroundExecutor,
19984 cx: &mut TestAppContext,
19985) {
19986 init_test(cx, |_| {});
19987
19988 let mut cx = EditorTestContext::new(cx).await;
19989
19990 let diff_base = r#"
19991 use some::mod1;
19992 use some::mod2;
19993
19994 const A: u32 = 42;
19995 const B: u32 = 42;
19996 const C: u32 = 42;
19997 const D: u32 = 42;
19998
19999
20000 fn main() {
20001 println!("hello");
20002
20003 println!("world");
20004 }"#
20005 .unindent();
20006
20007 cx.set_state(
20008 &r#"
20009 use some::mod1;
20010 use some::mod2;
20011
20012 const A: u32 = 42;
20013 const B: u32 = 42;
20014 const C: u32 = 43ˇ
20015 const D: u32 = 42;
20016
20017
20018 fn main() {
20019 println!("hello");
20020
20021 println!("world");
20022 }"#
20023 .unindent(),
20024 );
20025
20026 cx.set_head_text(&diff_base);
20027 executor.run_until_parked();
20028 cx.update_editor(|editor, window, cx| {
20029 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20030 });
20031 executor.run_until_parked();
20032
20033 cx.assert_state_with_diff(
20034 r#"
20035 use some::mod1;
20036 use some::mod2;
20037
20038 const A: u32 = 42;
20039 const B: u32 = 42;
20040 - const C: u32 = 42;
20041 + const C: u32 = 43ˇ
20042 const D: u32 = 42;
20043
20044
20045 fn main() {
20046 println!("hello");
20047
20048 println!("world");
20049 }"#
20050 .unindent(),
20051 );
20052
20053 cx.update_editor(|editor, window, cx| {
20054 editor.handle_input("\nnew_line\n", window, cx);
20055 });
20056 executor.run_until_parked();
20057
20058 cx.assert_state_with_diff(
20059 r#"
20060 use some::mod1;
20061 use some::mod2;
20062
20063 const A: u32 = 42;
20064 const B: u32 = 42;
20065 - const C: u32 = 42;
20066 + const C: u32 = 43
20067 + new_line
20068 + ˇ
20069 const D: u32 = 42;
20070
20071
20072 fn main() {
20073 println!("hello");
20074
20075 println!("world");
20076 }"#
20077 .unindent(),
20078 );
20079}
20080
20081#[gpui::test]
20082async fn test_stage_and_unstage_added_file_hunk(
20083 executor: BackgroundExecutor,
20084 cx: &mut TestAppContext,
20085) {
20086 init_test(cx, |_| {});
20087
20088 let mut cx = EditorTestContext::new(cx).await;
20089 cx.update_editor(|editor, _, cx| {
20090 editor.set_expand_all_diff_hunks(cx);
20091 });
20092
20093 let working_copy = r#"
20094 ˇfn main() {
20095 println!("hello, world!");
20096 }
20097 "#
20098 .unindent();
20099
20100 cx.set_state(&working_copy);
20101 executor.run_until_parked();
20102
20103 cx.assert_state_with_diff(
20104 r#"
20105 + ˇfn main() {
20106 + println!("hello, world!");
20107 + }
20108 "#
20109 .unindent(),
20110 );
20111 cx.assert_index_text(None);
20112
20113 cx.update_editor(|editor, window, cx| {
20114 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20115 });
20116 executor.run_until_parked();
20117 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20118 cx.assert_state_with_diff(
20119 r#"
20120 + ˇfn main() {
20121 + println!("hello, world!");
20122 + }
20123 "#
20124 .unindent(),
20125 );
20126
20127 cx.update_editor(|editor, window, cx| {
20128 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20129 });
20130 executor.run_until_parked();
20131 cx.assert_index_text(None);
20132}
20133
20134async fn setup_indent_guides_editor(
20135 text: &str,
20136 cx: &mut TestAppContext,
20137) -> (BufferId, EditorTestContext) {
20138 init_test(cx, |_| {});
20139
20140 let mut cx = EditorTestContext::new(cx).await;
20141
20142 let buffer_id = cx.update_editor(|editor, window, cx| {
20143 editor.set_text(text, window, cx);
20144 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20145
20146 buffer_ids[0]
20147 });
20148
20149 (buffer_id, cx)
20150}
20151
20152fn assert_indent_guides(
20153 range: Range<u32>,
20154 expected: Vec<IndentGuide>,
20155 active_indices: Option<Vec<usize>>,
20156 cx: &mut EditorTestContext,
20157) {
20158 let indent_guides = cx.update_editor(|editor, window, cx| {
20159 let snapshot = editor.snapshot(window, cx).display_snapshot;
20160 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20161 editor,
20162 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20163 true,
20164 &snapshot,
20165 cx,
20166 );
20167
20168 indent_guides.sort_by(|a, b| {
20169 a.depth.cmp(&b.depth).then(
20170 a.start_row
20171 .cmp(&b.start_row)
20172 .then(a.end_row.cmp(&b.end_row)),
20173 )
20174 });
20175 indent_guides
20176 });
20177
20178 if let Some(expected) = active_indices {
20179 let active_indices = cx.update_editor(|editor, window, cx| {
20180 let snapshot = editor.snapshot(window, cx).display_snapshot;
20181 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20182 });
20183
20184 assert_eq!(
20185 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20186 expected,
20187 "Active indent guide indices do not match"
20188 );
20189 }
20190
20191 assert_eq!(indent_guides, expected, "Indent guides do not match");
20192}
20193
20194fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20195 IndentGuide {
20196 buffer_id,
20197 start_row: MultiBufferRow(start_row),
20198 end_row: MultiBufferRow(end_row),
20199 depth,
20200 tab_size: 4,
20201 settings: IndentGuideSettings {
20202 enabled: true,
20203 line_width: 1,
20204 active_line_width: 1,
20205 coloring: IndentGuideColoring::default(),
20206 background_coloring: IndentGuideBackgroundColoring::default(),
20207 },
20208 }
20209}
20210
20211#[gpui::test]
20212async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20213 let (buffer_id, mut cx) = setup_indent_guides_editor(
20214 &"
20215 fn main() {
20216 let a = 1;
20217 }"
20218 .unindent(),
20219 cx,
20220 )
20221 .await;
20222
20223 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20224}
20225
20226#[gpui::test]
20227async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20228 let (buffer_id, mut cx) = setup_indent_guides_editor(
20229 &"
20230 fn main() {
20231 let a = 1;
20232 let b = 2;
20233 }"
20234 .unindent(),
20235 cx,
20236 )
20237 .await;
20238
20239 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20240}
20241
20242#[gpui::test]
20243async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20244 let (buffer_id, mut cx) = setup_indent_guides_editor(
20245 &"
20246 fn main() {
20247 let a = 1;
20248 if a == 3 {
20249 let b = 2;
20250 } else {
20251 let c = 3;
20252 }
20253 }"
20254 .unindent(),
20255 cx,
20256 )
20257 .await;
20258
20259 assert_indent_guides(
20260 0..8,
20261 vec![
20262 indent_guide(buffer_id, 1, 6, 0),
20263 indent_guide(buffer_id, 3, 3, 1),
20264 indent_guide(buffer_id, 5, 5, 1),
20265 ],
20266 None,
20267 &mut cx,
20268 );
20269}
20270
20271#[gpui::test]
20272async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20273 let (buffer_id, mut cx) = setup_indent_guides_editor(
20274 &"
20275 fn main() {
20276 let a = 1;
20277 let b = 2;
20278 let c = 3;
20279 }"
20280 .unindent(),
20281 cx,
20282 )
20283 .await;
20284
20285 assert_indent_guides(
20286 0..5,
20287 vec![
20288 indent_guide(buffer_id, 1, 3, 0),
20289 indent_guide(buffer_id, 2, 2, 1),
20290 ],
20291 None,
20292 &mut cx,
20293 );
20294}
20295
20296#[gpui::test]
20297async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20298 let (buffer_id, mut cx) = setup_indent_guides_editor(
20299 &"
20300 fn main() {
20301 let a = 1;
20302
20303 let c = 3;
20304 }"
20305 .unindent(),
20306 cx,
20307 )
20308 .await;
20309
20310 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20311}
20312
20313#[gpui::test]
20314async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20315 let (buffer_id, mut cx) = setup_indent_guides_editor(
20316 &"
20317 fn main() {
20318 let a = 1;
20319
20320 let c = 3;
20321
20322 if a == 3 {
20323 let b = 2;
20324 } else {
20325 let c = 3;
20326 }
20327 }"
20328 .unindent(),
20329 cx,
20330 )
20331 .await;
20332
20333 assert_indent_guides(
20334 0..11,
20335 vec![
20336 indent_guide(buffer_id, 1, 9, 0),
20337 indent_guide(buffer_id, 6, 6, 1),
20338 indent_guide(buffer_id, 8, 8, 1),
20339 ],
20340 None,
20341 &mut cx,
20342 );
20343}
20344
20345#[gpui::test]
20346async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20347 let (buffer_id, mut cx) = setup_indent_guides_editor(
20348 &"
20349 fn main() {
20350 let a = 1;
20351
20352 let c = 3;
20353
20354 if a == 3 {
20355 let b = 2;
20356 } else {
20357 let c = 3;
20358 }
20359 }"
20360 .unindent(),
20361 cx,
20362 )
20363 .await;
20364
20365 assert_indent_guides(
20366 1..11,
20367 vec![
20368 indent_guide(buffer_id, 1, 9, 0),
20369 indent_guide(buffer_id, 6, 6, 1),
20370 indent_guide(buffer_id, 8, 8, 1),
20371 ],
20372 None,
20373 &mut cx,
20374 );
20375}
20376
20377#[gpui::test]
20378async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20379 let (buffer_id, mut cx) = setup_indent_guides_editor(
20380 &"
20381 fn main() {
20382 let a = 1;
20383
20384 let c = 3;
20385
20386 if a == 3 {
20387 let b = 2;
20388 } else {
20389 let c = 3;
20390 }
20391 }"
20392 .unindent(),
20393 cx,
20394 )
20395 .await;
20396
20397 assert_indent_guides(
20398 1..10,
20399 vec![
20400 indent_guide(buffer_id, 1, 9, 0),
20401 indent_guide(buffer_id, 6, 6, 1),
20402 indent_guide(buffer_id, 8, 8, 1),
20403 ],
20404 None,
20405 &mut cx,
20406 );
20407}
20408
20409#[gpui::test]
20410async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20411 let (buffer_id, mut cx) = setup_indent_guides_editor(
20412 &"
20413 fn main() {
20414 if a {
20415 b(
20416 c,
20417 d,
20418 )
20419 } else {
20420 e(
20421 f
20422 )
20423 }
20424 }"
20425 .unindent(),
20426 cx,
20427 )
20428 .await;
20429
20430 assert_indent_guides(
20431 0..11,
20432 vec![
20433 indent_guide(buffer_id, 1, 10, 0),
20434 indent_guide(buffer_id, 2, 5, 1),
20435 indent_guide(buffer_id, 7, 9, 1),
20436 indent_guide(buffer_id, 3, 4, 2),
20437 indent_guide(buffer_id, 8, 8, 2),
20438 ],
20439 None,
20440 &mut cx,
20441 );
20442
20443 cx.update_editor(|editor, window, cx| {
20444 editor.fold_at(MultiBufferRow(2), window, cx);
20445 assert_eq!(
20446 editor.display_text(cx),
20447 "
20448 fn main() {
20449 if a {
20450 b(⋯
20451 )
20452 } else {
20453 e(
20454 f
20455 )
20456 }
20457 }"
20458 .unindent()
20459 );
20460 });
20461
20462 assert_indent_guides(
20463 0..11,
20464 vec![
20465 indent_guide(buffer_id, 1, 10, 0),
20466 indent_guide(buffer_id, 2, 5, 1),
20467 indent_guide(buffer_id, 7, 9, 1),
20468 indent_guide(buffer_id, 8, 8, 2),
20469 ],
20470 None,
20471 &mut cx,
20472 );
20473}
20474
20475#[gpui::test]
20476async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20477 let (buffer_id, mut cx) = setup_indent_guides_editor(
20478 &"
20479 block1
20480 block2
20481 block3
20482 block4
20483 block2
20484 block1
20485 block1"
20486 .unindent(),
20487 cx,
20488 )
20489 .await;
20490
20491 assert_indent_guides(
20492 1..10,
20493 vec![
20494 indent_guide(buffer_id, 1, 4, 0),
20495 indent_guide(buffer_id, 2, 3, 1),
20496 indent_guide(buffer_id, 3, 3, 2),
20497 ],
20498 None,
20499 &mut cx,
20500 );
20501}
20502
20503#[gpui::test]
20504async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20505 let (buffer_id, mut cx) = setup_indent_guides_editor(
20506 &"
20507 block1
20508 block2
20509 block3
20510
20511 block1
20512 block1"
20513 .unindent(),
20514 cx,
20515 )
20516 .await;
20517
20518 assert_indent_guides(
20519 0..6,
20520 vec![
20521 indent_guide(buffer_id, 1, 2, 0),
20522 indent_guide(buffer_id, 2, 2, 1),
20523 ],
20524 None,
20525 &mut cx,
20526 );
20527}
20528
20529#[gpui::test]
20530async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20531 let (buffer_id, mut cx) = setup_indent_guides_editor(
20532 &"
20533 function component() {
20534 \treturn (
20535 \t\t\t
20536 \t\t<div>
20537 \t\t\t<abc></abc>
20538 \t\t</div>
20539 \t)
20540 }"
20541 .unindent(),
20542 cx,
20543 )
20544 .await;
20545
20546 assert_indent_guides(
20547 0..8,
20548 vec![
20549 indent_guide(buffer_id, 1, 6, 0),
20550 indent_guide(buffer_id, 2, 5, 1),
20551 indent_guide(buffer_id, 4, 4, 2),
20552 ],
20553 None,
20554 &mut cx,
20555 );
20556}
20557
20558#[gpui::test]
20559async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20560 let (buffer_id, mut cx) = setup_indent_guides_editor(
20561 &"
20562 function component() {
20563 \treturn (
20564 \t
20565 \t\t<div>
20566 \t\t\t<abc></abc>
20567 \t\t</div>
20568 \t)
20569 }"
20570 .unindent(),
20571 cx,
20572 )
20573 .await;
20574
20575 assert_indent_guides(
20576 0..8,
20577 vec![
20578 indent_guide(buffer_id, 1, 6, 0),
20579 indent_guide(buffer_id, 2, 5, 1),
20580 indent_guide(buffer_id, 4, 4, 2),
20581 ],
20582 None,
20583 &mut cx,
20584 );
20585}
20586
20587#[gpui::test]
20588async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20589 let (buffer_id, mut cx) = setup_indent_guides_editor(
20590 &"
20591 block1
20592
20593
20594
20595 block2
20596 "
20597 .unindent(),
20598 cx,
20599 )
20600 .await;
20601
20602 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20603}
20604
20605#[gpui::test]
20606async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20607 let (buffer_id, mut cx) = setup_indent_guides_editor(
20608 &"
20609 def a:
20610 \tb = 3
20611 \tif True:
20612 \t\tc = 4
20613 \t\td = 5
20614 \tprint(b)
20615 "
20616 .unindent(),
20617 cx,
20618 )
20619 .await;
20620
20621 assert_indent_guides(
20622 0..6,
20623 vec![
20624 indent_guide(buffer_id, 1, 5, 0),
20625 indent_guide(buffer_id, 3, 4, 1),
20626 ],
20627 None,
20628 &mut cx,
20629 );
20630}
20631
20632#[gpui::test]
20633async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20634 let (buffer_id, mut cx) = setup_indent_guides_editor(
20635 &"
20636 fn main() {
20637 let a = 1;
20638 }"
20639 .unindent(),
20640 cx,
20641 )
20642 .await;
20643
20644 cx.update_editor(|editor, window, cx| {
20645 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20646 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20647 });
20648 });
20649
20650 assert_indent_guides(
20651 0..3,
20652 vec![indent_guide(buffer_id, 1, 1, 0)],
20653 Some(vec![0]),
20654 &mut cx,
20655 );
20656}
20657
20658#[gpui::test]
20659async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20660 let (buffer_id, mut cx) = setup_indent_guides_editor(
20661 &"
20662 fn main() {
20663 if 1 == 2 {
20664 let a = 1;
20665 }
20666 }"
20667 .unindent(),
20668 cx,
20669 )
20670 .await;
20671
20672 cx.update_editor(|editor, window, cx| {
20673 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20674 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20675 });
20676 });
20677
20678 assert_indent_guides(
20679 0..4,
20680 vec![
20681 indent_guide(buffer_id, 1, 3, 0),
20682 indent_guide(buffer_id, 2, 2, 1),
20683 ],
20684 Some(vec![1]),
20685 &mut cx,
20686 );
20687
20688 cx.update_editor(|editor, window, cx| {
20689 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20690 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20691 });
20692 });
20693
20694 assert_indent_guides(
20695 0..4,
20696 vec![
20697 indent_guide(buffer_id, 1, 3, 0),
20698 indent_guide(buffer_id, 2, 2, 1),
20699 ],
20700 Some(vec![1]),
20701 &mut cx,
20702 );
20703
20704 cx.update_editor(|editor, window, cx| {
20705 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20706 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20707 });
20708 });
20709
20710 assert_indent_guides(
20711 0..4,
20712 vec![
20713 indent_guide(buffer_id, 1, 3, 0),
20714 indent_guide(buffer_id, 2, 2, 1),
20715 ],
20716 Some(vec![0]),
20717 &mut cx,
20718 );
20719}
20720
20721#[gpui::test]
20722async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20723 let (buffer_id, mut cx) = setup_indent_guides_editor(
20724 &"
20725 fn main() {
20726 let a = 1;
20727
20728 let b = 2;
20729 }"
20730 .unindent(),
20731 cx,
20732 )
20733 .await;
20734
20735 cx.update_editor(|editor, window, cx| {
20736 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20737 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20738 });
20739 });
20740
20741 assert_indent_guides(
20742 0..5,
20743 vec![indent_guide(buffer_id, 1, 3, 0)],
20744 Some(vec![0]),
20745 &mut cx,
20746 );
20747}
20748
20749#[gpui::test]
20750async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20751 let (buffer_id, mut cx) = setup_indent_guides_editor(
20752 &"
20753 def m:
20754 a = 1
20755 pass"
20756 .unindent(),
20757 cx,
20758 )
20759 .await;
20760
20761 cx.update_editor(|editor, window, cx| {
20762 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20763 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20764 });
20765 });
20766
20767 assert_indent_guides(
20768 0..3,
20769 vec![indent_guide(buffer_id, 1, 2, 0)],
20770 Some(vec![0]),
20771 &mut cx,
20772 );
20773}
20774
20775#[gpui::test]
20776async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20777 init_test(cx, |_| {});
20778 let mut cx = EditorTestContext::new(cx).await;
20779 let text = indoc! {
20780 "
20781 impl A {
20782 fn b() {
20783 0;
20784 3;
20785 5;
20786 6;
20787 7;
20788 }
20789 }
20790 "
20791 };
20792 let base_text = indoc! {
20793 "
20794 impl A {
20795 fn b() {
20796 0;
20797 1;
20798 2;
20799 3;
20800 4;
20801 }
20802 fn c() {
20803 5;
20804 6;
20805 7;
20806 }
20807 }
20808 "
20809 };
20810
20811 cx.update_editor(|editor, window, cx| {
20812 editor.set_text(text, window, cx);
20813
20814 editor.buffer().update(cx, |multibuffer, cx| {
20815 let buffer = multibuffer.as_singleton().unwrap();
20816 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20817
20818 multibuffer.set_all_diff_hunks_expanded(cx);
20819 multibuffer.add_diff(diff, cx);
20820
20821 buffer.read(cx).remote_id()
20822 })
20823 });
20824 cx.run_until_parked();
20825
20826 cx.assert_state_with_diff(
20827 indoc! { "
20828 impl A {
20829 fn b() {
20830 0;
20831 - 1;
20832 - 2;
20833 3;
20834 - 4;
20835 - }
20836 - fn c() {
20837 5;
20838 6;
20839 7;
20840 }
20841 }
20842 ˇ"
20843 }
20844 .to_string(),
20845 );
20846
20847 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20848 editor
20849 .snapshot(window, cx)
20850 .buffer_snapshot()
20851 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20852 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20853 .collect::<Vec<_>>()
20854 });
20855 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20856 assert_eq!(
20857 actual_guides,
20858 vec![
20859 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20860 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20861 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20862 ]
20863 );
20864}
20865
20866#[gpui::test]
20867async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20868 init_test(cx, |_| {});
20869 let mut cx = EditorTestContext::new(cx).await;
20870
20871 let diff_base = r#"
20872 a
20873 b
20874 c
20875 "#
20876 .unindent();
20877
20878 cx.set_state(
20879 &r#"
20880 ˇA
20881 b
20882 C
20883 "#
20884 .unindent(),
20885 );
20886 cx.set_head_text(&diff_base);
20887 cx.update_editor(|editor, window, cx| {
20888 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20889 });
20890 executor.run_until_parked();
20891
20892 let both_hunks_expanded = r#"
20893 - a
20894 + ˇA
20895 b
20896 - c
20897 + C
20898 "#
20899 .unindent();
20900
20901 cx.assert_state_with_diff(both_hunks_expanded.clone());
20902
20903 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20904 let snapshot = editor.snapshot(window, cx);
20905 let hunks = editor
20906 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
20907 .collect::<Vec<_>>();
20908 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20909 let buffer_id = hunks[0].buffer_id;
20910 hunks
20911 .into_iter()
20912 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20913 .collect::<Vec<_>>()
20914 });
20915 assert_eq!(hunk_ranges.len(), 2);
20916
20917 cx.update_editor(|editor, _, cx| {
20918 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20919 });
20920 executor.run_until_parked();
20921
20922 let second_hunk_expanded = r#"
20923 ˇA
20924 b
20925 - c
20926 + C
20927 "#
20928 .unindent();
20929
20930 cx.assert_state_with_diff(second_hunk_expanded);
20931
20932 cx.update_editor(|editor, _, cx| {
20933 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20934 });
20935 executor.run_until_parked();
20936
20937 cx.assert_state_with_diff(both_hunks_expanded.clone());
20938
20939 cx.update_editor(|editor, _, cx| {
20940 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20941 });
20942 executor.run_until_parked();
20943
20944 let first_hunk_expanded = r#"
20945 - a
20946 + ˇA
20947 b
20948 C
20949 "#
20950 .unindent();
20951
20952 cx.assert_state_with_diff(first_hunk_expanded);
20953
20954 cx.update_editor(|editor, _, cx| {
20955 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20956 });
20957 executor.run_until_parked();
20958
20959 cx.assert_state_with_diff(both_hunks_expanded);
20960
20961 cx.set_state(
20962 &r#"
20963 ˇA
20964 b
20965 "#
20966 .unindent(),
20967 );
20968 cx.run_until_parked();
20969
20970 // TODO this cursor position seems bad
20971 cx.assert_state_with_diff(
20972 r#"
20973 - ˇa
20974 + A
20975 b
20976 "#
20977 .unindent(),
20978 );
20979
20980 cx.update_editor(|editor, window, cx| {
20981 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20982 });
20983
20984 cx.assert_state_with_diff(
20985 r#"
20986 - ˇa
20987 + A
20988 b
20989 - c
20990 "#
20991 .unindent(),
20992 );
20993
20994 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20995 let snapshot = editor.snapshot(window, cx);
20996 let hunks = editor
20997 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
20998 .collect::<Vec<_>>();
20999 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21000 let buffer_id = hunks[0].buffer_id;
21001 hunks
21002 .into_iter()
21003 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21004 .collect::<Vec<_>>()
21005 });
21006 assert_eq!(hunk_ranges.len(), 2);
21007
21008 cx.update_editor(|editor, _, cx| {
21009 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21010 });
21011 executor.run_until_parked();
21012
21013 cx.assert_state_with_diff(
21014 r#"
21015 - ˇa
21016 + A
21017 b
21018 "#
21019 .unindent(),
21020 );
21021}
21022
21023#[gpui::test]
21024async fn test_toggle_deletion_hunk_at_start_of_file(
21025 executor: BackgroundExecutor,
21026 cx: &mut TestAppContext,
21027) {
21028 init_test(cx, |_| {});
21029 let mut cx = EditorTestContext::new(cx).await;
21030
21031 let diff_base = r#"
21032 a
21033 b
21034 c
21035 "#
21036 .unindent();
21037
21038 cx.set_state(
21039 &r#"
21040 ˇb
21041 c
21042 "#
21043 .unindent(),
21044 );
21045 cx.set_head_text(&diff_base);
21046 cx.update_editor(|editor, window, cx| {
21047 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21048 });
21049 executor.run_until_parked();
21050
21051 let hunk_expanded = r#"
21052 - a
21053 ˇb
21054 c
21055 "#
21056 .unindent();
21057
21058 cx.assert_state_with_diff(hunk_expanded.clone());
21059
21060 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21061 let snapshot = editor.snapshot(window, cx);
21062 let hunks = editor
21063 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21064 .collect::<Vec<_>>();
21065 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21066 let buffer_id = hunks[0].buffer_id;
21067 hunks
21068 .into_iter()
21069 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21070 .collect::<Vec<_>>()
21071 });
21072 assert_eq!(hunk_ranges.len(), 1);
21073
21074 cx.update_editor(|editor, _, cx| {
21075 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21076 });
21077 executor.run_until_parked();
21078
21079 let hunk_collapsed = r#"
21080 ˇb
21081 c
21082 "#
21083 .unindent();
21084
21085 cx.assert_state_with_diff(hunk_collapsed);
21086
21087 cx.update_editor(|editor, _, cx| {
21088 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21089 });
21090 executor.run_until_parked();
21091
21092 cx.assert_state_with_diff(hunk_expanded);
21093}
21094
21095#[gpui::test]
21096async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21097 init_test(cx, |_| {});
21098
21099 let fs = FakeFs::new(cx.executor());
21100 fs.insert_tree(
21101 path!("/test"),
21102 json!({
21103 ".git": {},
21104 "file-1": "ONE\n",
21105 "file-2": "TWO\n",
21106 "file-3": "THREE\n",
21107 }),
21108 )
21109 .await;
21110
21111 fs.set_head_for_repo(
21112 path!("/test/.git").as_ref(),
21113 &[
21114 ("file-1", "one\n".into()),
21115 ("file-2", "two\n".into()),
21116 ("file-3", "three\n".into()),
21117 ],
21118 "deadbeef",
21119 );
21120
21121 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21122 let mut buffers = vec![];
21123 for i in 1..=3 {
21124 let buffer = project
21125 .update(cx, |project, cx| {
21126 let path = format!(path!("/test/file-{}"), i);
21127 project.open_local_buffer(path, cx)
21128 })
21129 .await
21130 .unwrap();
21131 buffers.push(buffer);
21132 }
21133
21134 let multibuffer = cx.new(|cx| {
21135 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21136 multibuffer.set_all_diff_hunks_expanded(cx);
21137 for buffer in &buffers {
21138 let snapshot = buffer.read(cx).snapshot();
21139 multibuffer.set_excerpts_for_path(
21140 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21141 buffer.clone(),
21142 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21143 2,
21144 cx,
21145 );
21146 }
21147 multibuffer
21148 });
21149
21150 let editor = cx.add_window(|window, cx| {
21151 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21152 });
21153 cx.run_until_parked();
21154
21155 let snapshot = editor
21156 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21157 .unwrap();
21158 let hunks = snapshot
21159 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21160 .map(|hunk| match hunk {
21161 DisplayDiffHunk::Unfolded {
21162 display_row_range, ..
21163 } => display_row_range,
21164 DisplayDiffHunk::Folded { .. } => unreachable!(),
21165 })
21166 .collect::<Vec<_>>();
21167 assert_eq!(
21168 hunks,
21169 [
21170 DisplayRow(2)..DisplayRow(4),
21171 DisplayRow(7)..DisplayRow(9),
21172 DisplayRow(12)..DisplayRow(14),
21173 ]
21174 );
21175}
21176
21177#[gpui::test]
21178async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21179 init_test(cx, |_| {});
21180
21181 let mut cx = EditorTestContext::new(cx).await;
21182 cx.set_head_text(indoc! { "
21183 one
21184 two
21185 three
21186 four
21187 five
21188 "
21189 });
21190 cx.set_index_text(indoc! { "
21191 one
21192 two
21193 three
21194 four
21195 five
21196 "
21197 });
21198 cx.set_state(indoc! {"
21199 one
21200 TWO
21201 ˇTHREE
21202 FOUR
21203 five
21204 "});
21205 cx.run_until_parked();
21206 cx.update_editor(|editor, window, cx| {
21207 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21208 });
21209 cx.run_until_parked();
21210 cx.assert_index_text(Some(indoc! {"
21211 one
21212 TWO
21213 THREE
21214 FOUR
21215 five
21216 "}));
21217 cx.set_state(indoc! { "
21218 one
21219 TWO
21220 ˇTHREE-HUNDRED
21221 FOUR
21222 five
21223 "});
21224 cx.run_until_parked();
21225 cx.update_editor(|editor, window, cx| {
21226 let snapshot = editor.snapshot(window, cx);
21227 let hunks = editor
21228 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21229 .collect::<Vec<_>>();
21230 assert_eq!(hunks.len(), 1);
21231 assert_eq!(
21232 hunks[0].status(),
21233 DiffHunkStatus {
21234 kind: DiffHunkStatusKind::Modified,
21235 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21236 }
21237 );
21238
21239 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21240 });
21241 cx.run_until_parked();
21242 cx.assert_index_text(Some(indoc! {"
21243 one
21244 TWO
21245 THREE-HUNDRED
21246 FOUR
21247 five
21248 "}));
21249}
21250
21251#[gpui::test]
21252fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21253 init_test(cx, |_| {});
21254
21255 let editor = cx.add_window(|window, cx| {
21256 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21257 build_editor(buffer, window, cx)
21258 });
21259
21260 let render_args = Arc::new(Mutex::new(None));
21261 let snapshot = editor
21262 .update(cx, |editor, window, cx| {
21263 let snapshot = editor.buffer().read(cx).snapshot(cx);
21264 let range =
21265 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21266
21267 struct RenderArgs {
21268 row: MultiBufferRow,
21269 folded: bool,
21270 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21271 }
21272
21273 let crease = Crease::inline(
21274 range,
21275 FoldPlaceholder::test(),
21276 {
21277 let toggle_callback = render_args.clone();
21278 move |row, folded, callback, _window, _cx| {
21279 *toggle_callback.lock() = Some(RenderArgs {
21280 row,
21281 folded,
21282 callback,
21283 });
21284 div()
21285 }
21286 },
21287 |_row, _folded, _window, _cx| div(),
21288 );
21289
21290 editor.insert_creases(Some(crease), cx);
21291 let snapshot = editor.snapshot(window, cx);
21292 let _div =
21293 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21294 snapshot
21295 })
21296 .unwrap();
21297
21298 let render_args = render_args.lock().take().unwrap();
21299 assert_eq!(render_args.row, MultiBufferRow(1));
21300 assert!(!render_args.folded);
21301 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21302
21303 cx.update_window(*editor, |_, window, cx| {
21304 (render_args.callback)(true, window, cx)
21305 })
21306 .unwrap();
21307 let snapshot = editor
21308 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21309 .unwrap();
21310 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21311
21312 cx.update_window(*editor, |_, window, cx| {
21313 (render_args.callback)(false, window, cx)
21314 })
21315 .unwrap();
21316 let snapshot = editor
21317 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21318 .unwrap();
21319 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21320}
21321
21322#[gpui::test]
21323async fn test_input_text(cx: &mut TestAppContext) {
21324 init_test(cx, |_| {});
21325 let mut cx = EditorTestContext::new(cx).await;
21326
21327 cx.set_state(
21328 &r#"ˇone
21329 two
21330
21331 three
21332 fourˇ
21333 five
21334
21335 siˇx"#
21336 .unindent(),
21337 );
21338
21339 cx.dispatch_action(HandleInput(String::new()));
21340 cx.assert_editor_state(
21341 &r#"ˇone
21342 two
21343
21344 three
21345 fourˇ
21346 five
21347
21348 siˇx"#
21349 .unindent(),
21350 );
21351
21352 cx.dispatch_action(HandleInput("AAAA".to_string()));
21353 cx.assert_editor_state(
21354 &r#"AAAAˇone
21355 two
21356
21357 three
21358 fourAAAAˇ
21359 five
21360
21361 siAAAAˇx"#
21362 .unindent(),
21363 );
21364}
21365
21366#[gpui::test]
21367async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21368 init_test(cx, |_| {});
21369
21370 let mut cx = EditorTestContext::new(cx).await;
21371 cx.set_state(
21372 r#"let foo = 1;
21373let foo = 2;
21374let foo = 3;
21375let fooˇ = 4;
21376let foo = 5;
21377let foo = 6;
21378let foo = 7;
21379let foo = 8;
21380let foo = 9;
21381let foo = 10;
21382let foo = 11;
21383let foo = 12;
21384let foo = 13;
21385let foo = 14;
21386let foo = 15;"#,
21387 );
21388
21389 cx.update_editor(|e, window, cx| {
21390 assert_eq!(
21391 e.next_scroll_position,
21392 NextScrollCursorCenterTopBottom::Center,
21393 "Default next scroll direction is center",
21394 );
21395
21396 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21397 assert_eq!(
21398 e.next_scroll_position,
21399 NextScrollCursorCenterTopBottom::Top,
21400 "After center, next scroll direction should be top",
21401 );
21402
21403 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21404 assert_eq!(
21405 e.next_scroll_position,
21406 NextScrollCursorCenterTopBottom::Bottom,
21407 "After top, next scroll direction should be bottom",
21408 );
21409
21410 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21411 assert_eq!(
21412 e.next_scroll_position,
21413 NextScrollCursorCenterTopBottom::Center,
21414 "After bottom, scrolling should start over",
21415 );
21416
21417 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21418 assert_eq!(
21419 e.next_scroll_position,
21420 NextScrollCursorCenterTopBottom::Top,
21421 "Scrolling continues if retriggered fast enough"
21422 );
21423 });
21424
21425 cx.executor()
21426 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21427 cx.executor().run_until_parked();
21428 cx.update_editor(|e, _, _| {
21429 assert_eq!(
21430 e.next_scroll_position,
21431 NextScrollCursorCenterTopBottom::Center,
21432 "If scrolling is not triggered fast enough, it should reset"
21433 );
21434 });
21435}
21436
21437#[gpui::test]
21438async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21439 init_test(cx, |_| {});
21440 let mut cx = EditorLspTestContext::new_rust(
21441 lsp::ServerCapabilities {
21442 definition_provider: Some(lsp::OneOf::Left(true)),
21443 references_provider: Some(lsp::OneOf::Left(true)),
21444 ..lsp::ServerCapabilities::default()
21445 },
21446 cx,
21447 )
21448 .await;
21449
21450 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21451 let go_to_definition = cx
21452 .lsp
21453 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21454 move |params, _| async move {
21455 if empty_go_to_definition {
21456 Ok(None)
21457 } else {
21458 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21459 uri: params.text_document_position_params.text_document.uri,
21460 range: lsp::Range::new(
21461 lsp::Position::new(4, 3),
21462 lsp::Position::new(4, 6),
21463 ),
21464 })))
21465 }
21466 },
21467 );
21468 let references = cx
21469 .lsp
21470 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21471 Ok(Some(vec![lsp::Location {
21472 uri: params.text_document_position.text_document.uri,
21473 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21474 }]))
21475 });
21476 (go_to_definition, references)
21477 };
21478
21479 cx.set_state(
21480 &r#"fn one() {
21481 let mut a = ˇtwo();
21482 }
21483
21484 fn two() {}"#
21485 .unindent(),
21486 );
21487 set_up_lsp_handlers(false, &mut cx);
21488 let navigated = cx
21489 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21490 .await
21491 .expect("Failed to navigate to definition");
21492 assert_eq!(
21493 navigated,
21494 Navigated::Yes,
21495 "Should have navigated to definition from the GetDefinition response"
21496 );
21497 cx.assert_editor_state(
21498 &r#"fn one() {
21499 let mut a = two();
21500 }
21501
21502 fn «twoˇ»() {}"#
21503 .unindent(),
21504 );
21505
21506 let editors = cx.update_workspace(|workspace, _, cx| {
21507 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21508 });
21509 cx.update_editor(|_, _, test_editor_cx| {
21510 assert_eq!(
21511 editors.len(),
21512 1,
21513 "Initially, only one, test, editor should be open in the workspace"
21514 );
21515 assert_eq!(
21516 test_editor_cx.entity(),
21517 editors.last().expect("Asserted len is 1").clone()
21518 );
21519 });
21520
21521 set_up_lsp_handlers(true, &mut cx);
21522 let navigated = cx
21523 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21524 .await
21525 .expect("Failed to navigate to lookup references");
21526 assert_eq!(
21527 navigated,
21528 Navigated::Yes,
21529 "Should have navigated to references as a fallback after empty GoToDefinition response"
21530 );
21531 // We should not change the selections in the existing file,
21532 // if opening another milti buffer with the references
21533 cx.assert_editor_state(
21534 &r#"fn one() {
21535 let mut a = two();
21536 }
21537
21538 fn «twoˇ»() {}"#
21539 .unindent(),
21540 );
21541 let editors = cx.update_workspace(|workspace, _, cx| {
21542 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21543 });
21544 cx.update_editor(|_, _, test_editor_cx| {
21545 assert_eq!(
21546 editors.len(),
21547 2,
21548 "After falling back to references search, we open a new editor with the results"
21549 );
21550 let references_fallback_text = editors
21551 .into_iter()
21552 .find(|new_editor| *new_editor != test_editor_cx.entity())
21553 .expect("Should have one non-test editor now")
21554 .read(test_editor_cx)
21555 .text(test_editor_cx);
21556 assert_eq!(
21557 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21558 "Should use the range from the references response and not the GoToDefinition one"
21559 );
21560 });
21561}
21562
21563#[gpui::test]
21564async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21565 init_test(cx, |_| {});
21566 cx.update(|cx| {
21567 let mut editor_settings = EditorSettings::get_global(cx).clone();
21568 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21569 EditorSettings::override_global(editor_settings, cx);
21570 });
21571 let mut cx = EditorLspTestContext::new_rust(
21572 lsp::ServerCapabilities {
21573 definition_provider: Some(lsp::OneOf::Left(true)),
21574 references_provider: Some(lsp::OneOf::Left(true)),
21575 ..lsp::ServerCapabilities::default()
21576 },
21577 cx,
21578 )
21579 .await;
21580 let original_state = r#"fn one() {
21581 let mut a = ˇtwo();
21582 }
21583
21584 fn two() {}"#
21585 .unindent();
21586 cx.set_state(&original_state);
21587
21588 let mut go_to_definition = cx
21589 .lsp
21590 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21591 move |_, _| async move { Ok(None) },
21592 );
21593 let _references = cx
21594 .lsp
21595 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21596 panic!("Should not call for references with no go to definition fallback")
21597 });
21598
21599 let navigated = cx
21600 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21601 .await
21602 .expect("Failed to navigate to lookup references");
21603 go_to_definition
21604 .next()
21605 .await
21606 .expect("Should have called the go_to_definition handler");
21607
21608 assert_eq!(
21609 navigated,
21610 Navigated::No,
21611 "Should have navigated to references as a fallback after empty GoToDefinition response"
21612 );
21613 cx.assert_editor_state(&original_state);
21614 let editors = cx.update_workspace(|workspace, _, cx| {
21615 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21616 });
21617 cx.update_editor(|_, _, _| {
21618 assert_eq!(
21619 editors.len(),
21620 1,
21621 "After unsuccessful fallback, no other editor should have been opened"
21622 );
21623 });
21624}
21625
21626#[gpui::test]
21627async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21628 init_test(cx, |_| {});
21629 let mut cx = EditorLspTestContext::new_rust(
21630 lsp::ServerCapabilities {
21631 references_provider: Some(lsp::OneOf::Left(true)),
21632 ..lsp::ServerCapabilities::default()
21633 },
21634 cx,
21635 )
21636 .await;
21637
21638 cx.set_state(
21639 &r#"
21640 fn one() {
21641 let mut a = two();
21642 }
21643
21644 fn ˇtwo() {}"#
21645 .unindent(),
21646 );
21647 cx.lsp
21648 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21649 Ok(Some(vec![
21650 lsp::Location {
21651 uri: params.text_document_position.text_document.uri.clone(),
21652 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21653 },
21654 lsp::Location {
21655 uri: params.text_document_position.text_document.uri,
21656 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21657 },
21658 ]))
21659 });
21660 let navigated = cx
21661 .update_editor(|editor, window, cx| {
21662 editor.find_all_references(&FindAllReferences, window, cx)
21663 })
21664 .unwrap()
21665 .await
21666 .expect("Failed to navigate to references");
21667 assert_eq!(
21668 navigated,
21669 Navigated::Yes,
21670 "Should have navigated to references from the FindAllReferences response"
21671 );
21672 cx.assert_editor_state(
21673 &r#"fn one() {
21674 let mut a = two();
21675 }
21676
21677 fn ˇtwo() {}"#
21678 .unindent(),
21679 );
21680
21681 let editors = cx.update_workspace(|workspace, _, cx| {
21682 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21683 });
21684 cx.update_editor(|_, _, _| {
21685 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21686 });
21687
21688 cx.set_state(
21689 &r#"fn one() {
21690 let mut a = ˇtwo();
21691 }
21692
21693 fn two() {}"#
21694 .unindent(),
21695 );
21696 let navigated = cx
21697 .update_editor(|editor, window, cx| {
21698 editor.find_all_references(&FindAllReferences, window, cx)
21699 })
21700 .unwrap()
21701 .await
21702 .expect("Failed to navigate to references");
21703 assert_eq!(
21704 navigated,
21705 Navigated::Yes,
21706 "Should have navigated to references from the FindAllReferences response"
21707 );
21708 cx.assert_editor_state(
21709 &r#"fn one() {
21710 let mut a = ˇtwo();
21711 }
21712
21713 fn two() {}"#
21714 .unindent(),
21715 );
21716 let editors = cx.update_workspace(|workspace, _, cx| {
21717 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21718 });
21719 cx.update_editor(|_, _, _| {
21720 assert_eq!(
21721 editors.len(),
21722 2,
21723 "should have re-used the previous multibuffer"
21724 );
21725 });
21726
21727 cx.set_state(
21728 &r#"fn one() {
21729 let mut a = ˇtwo();
21730 }
21731 fn three() {}
21732 fn two() {}"#
21733 .unindent(),
21734 );
21735 cx.lsp
21736 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21737 Ok(Some(vec![
21738 lsp::Location {
21739 uri: params.text_document_position.text_document.uri.clone(),
21740 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21741 },
21742 lsp::Location {
21743 uri: params.text_document_position.text_document.uri,
21744 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21745 },
21746 ]))
21747 });
21748 let navigated = cx
21749 .update_editor(|editor, window, cx| {
21750 editor.find_all_references(&FindAllReferences, window, cx)
21751 })
21752 .unwrap()
21753 .await
21754 .expect("Failed to navigate to references");
21755 assert_eq!(
21756 navigated,
21757 Navigated::Yes,
21758 "Should have navigated to references from the FindAllReferences response"
21759 );
21760 cx.assert_editor_state(
21761 &r#"fn one() {
21762 let mut a = ˇtwo();
21763 }
21764 fn three() {}
21765 fn two() {}"#
21766 .unindent(),
21767 );
21768 let editors = cx.update_workspace(|workspace, _, cx| {
21769 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21770 });
21771 cx.update_editor(|_, _, _| {
21772 assert_eq!(
21773 editors.len(),
21774 3,
21775 "should have used a new multibuffer as offsets changed"
21776 );
21777 });
21778}
21779#[gpui::test]
21780async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21781 init_test(cx, |_| {});
21782
21783 let language = Arc::new(Language::new(
21784 LanguageConfig::default(),
21785 Some(tree_sitter_rust::LANGUAGE.into()),
21786 ));
21787
21788 let text = r#"
21789 #[cfg(test)]
21790 mod tests() {
21791 #[test]
21792 fn runnable_1() {
21793 let a = 1;
21794 }
21795
21796 #[test]
21797 fn runnable_2() {
21798 let a = 1;
21799 let b = 2;
21800 }
21801 }
21802 "#
21803 .unindent();
21804
21805 let fs = FakeFs::new(cx.executor());
21806 fs.insert_file("/file.rs", Default::default()).await;
21807
21808 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21809 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21810 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21811 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21812 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21813
21814 let editor = cx.new_window_entity(|window, cx| {
21815 Editor::new(
21816 EditorMode::full(),
21817 multi_buffer,
21818 Some(project.clone()),
21819 window,
21820 cx,
21821 )
21822 });
21823
21824 editor.update_in(cx, |editor, window, cx| {
21825 let snapshot = editor.buffer().read(cx).snapshot(cx);
21826 editor.tasks.insert(
21827 (buffer.read(cx).remote_id(), 3),
21828 RunnableTasks {
21829 templates: vec![],
21830 offset: snapshot.anchor_before(43),
21831 column: 0,
21832 extra_variables: HashMap::default(),
21833 context_range: BufferOffset(43)..BufferOffset(85),
21834 },
21835 );
21836 editor.tasks.insert(
21837 (buffer.read(cx).remote_id(), 8),
21838 RunnableTasks {
21839 templates: vec![],
21840 offset: snapshot.anchor_before(86),
21841 column: 0,
21842 extra_variables: HashMap::default(),
21843 context_range: BufferOffset(86)..BufferOffset(191),
21844 },
21845 );
21846
21847 // Test finding task when cursor is inside function body
21848 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21849 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21850 });
21851 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21852 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21853
21854 // Test finding task when cursor is on function name
21855 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21856 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21857 });
21858 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21859 assert_eq!(row, 8, "Should find task when cursor is on function name");
21860 });
21861}
21862
21863#[gpui::test]
21864async fn test_folding_buffers(cx: &mut TestAppContext) {
21865 init_test(cx, |_| {});
21866
21867 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21868 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21869 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21870
21871 let fs = FakeFs::new(cx.executor());
21872 fs.insert_tree(
21873 path!("/a"),
21874 json!({
21875 "first.rs": sample_text_1,
21876 "second.rs": sample_text_2,
21877 "third.rs": sample_text_3,
21878 }),
21879 )
21880 .await;
21881 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21882 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21883 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21884 let worktree = project.update(cx, |project, cx| {
21885 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21886 assert_eq!(worktrees.len(), 1);
21887 worktrees.pop().unwrap()
21888 });
21889 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21890
21891 let buffer_1 = project
21892 .update(cx, |project, cx| {
21893 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
21894 })
21895 .await
21896 .unwrap();
21897 let buffer_2 = project
21898 .update(cx, |project, cx| {
21899 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
21900 })
21901 .await
21902 .unwrap();
21903 let buffer_3 = project
21904 .update(cx, |project, cx| {
21905 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
21906 })
21907 .await
21908 .unwrap();
21909
21910 let multi_buffer = cx.new(|cx| {
21911 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21912 multi_buffer.push_excerpts(
21913 buffer_1.clone(),
21914 [
21915 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21916 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21917 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21918 ],
21919 cx,
21920 );
21921 multi_buffer.push_excerpts(
21922 buffer_2.clone(),
21923 [
21924 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21925 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21926 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21927 ],
21928 cx,
21929 );
21930 multi_buffer.push_excerpts(
21931 buffer_3.clone(),
21932 [
21933 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21934 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21935 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21936 ],
21937 cx,
21938 );
21939 multi_buffer
21940 });
21941 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21942 Editor::new(
21943 EditorMode::full(),
21944 multi_buffer.clone(),
21945 Some(project.clone()),
21946 window,
21947 cx,
21948 )
21949 });
21950
21951 assert_eq!(
21952 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21953 "\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",
21954 );
21955
21956 multi_buffer_editor.update(cx, |editor, cx| {
21957 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21958 });
21959 assert_eq!(
21960 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21961 "\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",
21962 "After folding the first buffer, its text should not be displayed"
21963 );
21964
21965 multi_buffer_editor.update(cx, |editor, cx| {
21966 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21967 });
21968 assert_eq!(
21969 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21970 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21971 "After folding the second buffer, its text should not be displayed"
21972 );
21973
21974 multi_buffer_editor.update(cx, |editor, cx| {
21975 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21976 });
21977 assert_eq!(
21978 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21979 "\n\n\n\n\n",
21980 "After folding the third buffer, its text should not be displayed"
21981 );
21982
21983 // Emulate selection inside the fold logic, that should work
21984 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21985 editor
21986 .snapshot(window, cx)
21987 .next_line_boundary(Point::new(0, 4));
21988 });
21989
21990 multi_buffer_editor.update(cx, |editor, cx| {
21991 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21992 });
21993 assert_eq!(
21994 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21995 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21996 "After unfolding the second buffer, its text should be displayed"
21997 );
21998
21999 // Typing inside of buffer 1 causes that buffer to be unfolded.
22000 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22001 assert_eq!(
22002 multi_buffer
22003 .read(cx)
22004 .snapshot(cx)
22005 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22006 .collect::<String>(),
22007 "bbbb"
22008 );
22009 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22010 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22011 });
22012 editor.handle_input("B", window, cx);
22013 });
22014
22015 assert_eq!(
22016 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22017 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22018 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22019 );
22020
22021 multi_buffer_editor.update(cx, |editor, cx| {
22022 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22023 });
22024 assert_eq!(
22025 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22026 "\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",
22027 "After unfolding the all buffers, all original text should be displayed"
22028 );
22029}
22030
22031#[gpui::test]
22032async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22033 init_test(cx, |_| {});
22034
22035 let sample_text_1 = "1111\n2222\n3333".to_string();
22036 let sample_text_2 = "4444\n5555\n6666".to_string();
22037 let sample_text_3 = "7777\n8888\n9999".to_string();
22038
22039 let fs = FakeFs::new(cx.executor());
22040 fs.insert_tree(
22041 path!("/a"),
22042 json!({
22043 "first.rs": sample_text_1,
22044 "second.rs": sample_text_2,
22045 "third.rs": sample_text_3,
22046 }),
22047 )
22048 .await;
22049 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22050 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22051 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22052 let worktree = project.update(cx, |project, cx| {
22053 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22054 assert_eq!(worktrees.len(), 1);
22055 worktrees.pop().unwrap()
22056 });
22057 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22058
22059 let buffer_1 = project
22060 .update(cx, |project, cx| {
22061 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22062 })
22063 .await
22064 .unwrap();
22065 let buffer_2 = project
22066 .update(cx, |project, cx| {
22067 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22068 })
22069 .await
22070 .unwrap();
22071 let buffer_3 = project
22072 .update(cx, |project, cx| {
22073 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22074 })
22075 .await
22076 .unwrap();
22077
22078 let multi_buffer = cx.new(|cx| {
22079 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22080 multi_buffer.push_excerpts(
22081 buffer_1.clone(),
22082 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22083 cx,
22084 );
22085 multi_buffer.push_excerpts(
22086 buffer_2.clone(),
22087 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22088 cx,
22089 );
22090 multi_buffer.push_excerpts(
22091 buffer_3.clone(),
22092 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22093 cx,
22094 );
22095 multi_buffer
22096 });
22097
22098 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22099 Editor::new(
22100 EditorMode::full(),
22101 multi_buffer,
22102 Some(project.clone()),
22103 window,
22104 cx,
22105 )
22106 });
22107
22108 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22109 assert_eq!(
22110 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22111 full_text,
22112 );
22113
22114 multi_buffer_editor.update(cx, |editor, cx| {
22115 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22116 });
22117 assert_eq!(
22118 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22119 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22120 "After folding the first buffer, its text should not be displayed"
22121 );
22122
22123 multi_buffer_editor.update(cx, |editor, cx| {
22124 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22125 });
22126
22127 assert_eq!(
22128 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22129 "\n\n\n\n\n\n7777\n8888\n9999",
22130 "After folding the second buffer, its text should not be displayed"
22131 );
22132
22133 multi_buffer_editor.update(cx, |editor, cx| {
22134 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22135 });
22136 assert_eq!(
22137 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22138 "\n\n\n\n\n",
22139 "After folding the third buffer, its text should not be displayed"
22140 );
22141
22142 multi_buffer_editor.update(cx, |editor, cx| {
22143 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22144 });
22145 assert_eq!(
22146 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22147 "\n\n\n\n4444\n5555\n6666\n\n",
22148 "After unfolding the second buffer, its text should be displayed"
22149 );
22150
22151 multi_buffer_editor.update(cx, |editor, cx| {
22152 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22153 });
22154 assert_eq!(
22155 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22156 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22157 "After unfolding the first buffer, its text should be displayed"
22158 );
22159
22160 multi_buffer_editor.update(cx, |editor, cx| {
22161 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22162 });
22163 assert_eq!(
22164 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22165 full_text,
22166 "After unfolding all buffers, all original text should be displayed"
22167 );
22168}
22169
22170#[gpui::test]
22171async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22172 init_test(cx, |_| {});
22173
22174 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22175
22176 let fs = FakeFs::new(cx.executor());
22177 fs.insert_tree(
22178 path!("/a"),
22179 json!({
22180 "main.rs": sample_text,
22181 }),
22182 )
22183 .await;
22184 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22185 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22186 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22187 let worktree = project.update(cx, |project, cx| {
22188 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22189 assert_eq!(worktrees.len(), 1);
22190 worktrees.pop().unwrap()
22191 });
22192 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22193
22194 let buffer_1 = project
22195 .update(cx, |project, cx| {
22196 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22197 })
22198 .await
22199 .unwrap();
22200
22201 let multi_buffer = cx.new(|cx| {
22202 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22203 multi_buffer.push_excerpts(
22204 buffer_1.clone(),
22205 [ExcerptRange::new(
22206 Point::new(0, 0)
22207 ..Point::new(
22208 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22209 0,
22210 ),
22211 )],
22212 cx,
22213 );
22214 multi_buffer
22215 });
22216 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22217 Editor::new(
22218 EditorMode::full(),
22219 multi_buffer,
22220 Some(project.clone()),
22221 window,
22222 cx,
22223 )
22224 });
22225
22226 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22227 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22228 enum TestHighlight {}
22229 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22230 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22231 editor.highlight_text::<TestHighlight>(
22232 vec![highlight_range.clone()],
22233 HighlightStyle::color(Hsla::green()),
22234 cx,
22235 );
22236 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22237 s.select_ranges(Some(highlight_range))
22238 });
22239 });
22240
22241 let full_text = format!("\n\n{sample_text}");
22242 assert_eq!(
22243 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22244 full_text,
22245 );
22246}
22247
22248#[gpui::test]
22249async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22250 init_test(cx, |_| {});
22251 cx.update(|cx| {
22252 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22253 "keymaps/default-linux.json",
22254 cx,
22255 )
22256 .unwrap();
22257 cx.bind_keys(default_key_bindings);
22258 });
22259
22260 let (editor, cx) = cx.add_window_view(|window, cx| {
22261 let multi_buffer = MultiBuffer::build_multi(
22262 [
22263 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22264 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22265 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22266 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22267 ],
22268 cx,
22269 );
22270 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22271
22272 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22273 // fold all but the second buffer, so that we test navigating between two
22274 // adjacent folded buffers, as well as folded buffers at the start and
22275 // end the multibuffer
22276 editor.fold_buffer(buffer_ids[0], cx);
22277 editor.fold_buffer(buffer_ids[2], cx);
22278 editor.fold_buffer(buffer_ids[3], cx);
22279
22280 editor
22281 });
22282 cx.simulate_resize(size(px(1000.), px(1000.)));
22283
22284 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22285 cx.assert_excerpts_with_selections(indoc! {"
22286 [EXCERPT]
22287 ˇ[FOLDED]
22288 [EXCERPT]
22289 a1
22290 b1
22291 [EXCERPT]
22292 [FOLDED]
22293 [EXCERPT]
22294 [FOLDED]
22295 "
22296 });
22297 cx.simulate_keystroke("down");
22298 cx.assert_excerpts_with_selections(indoc! {"
22299 [EXCERPT]
22300 [FOLDED]
22301 [EXCERPT]
22302 ˇa1
22303 b1
22304 [EXCERPT]
22305 [FOLDED]
22306 [EXCERPT]
22307 [FOLDED]
22308 "
22309 });
22310 cx.simulate_keystroke("down");
22311 cx.assert_excerpts_with_selections(indoc! {"
22312 [EXCERPT]
22313 [FOLDED]
22314 [EXCERPT]
22315 a1
22316 ˇb1
22317 [EXCERPT]
22318 [FOLDED]
22319 [EXCERPT]
22320 [FOLDED]
22321 "
22322 });
22323 cx.simulate_keystroke("down");
22324 cx.assert_excerpts_with_selections(indoc! {"
22325 [EXCERPT]
22326 [FOLDED]
22327 [EXCERPT]
22328 a1
22329 b1
22330 ˇ[EXCERPT]
22331 [FOLDED]
22332 [EXCERPT]
22333 [FOLDED]
22334 "
22335 });
22336 cx.simulate_keystroke("down");
22337 cx.assert_excerpts_with_selections(indoc! {"
22338 [EXCERPT]
22339 [FOLDED]
22340 [EXCERPT]
22341 a1
22342 b1
22343 [EXCERPT]
22344 ˇ[FOLDED]
22345 [EXCERPT]
22346 [FOLDED]
22347 "
22348 });
22349 for _ in 0..5 {
22350 cx.simulate_keystroke("down");
22351 cx.assert_excerpts_with_selections(indoc! {"
22352 [EXCERPT]
22353 [FOLDED]
22354 [EXCERPT]
22355 a1
22356 b1
22357 [EXCERPT]
22358 [FOLDED]
22359 [EXCERPT]
22360 ˇ[FOLDED]
22361 "
22362 });
22363 }
22364
22365 cx.simulate_keystroke("up");
22366 cx.assert_excerpts_with_selections(indoc! {"
22367 [EXCERPT]
22368 [FOLDED]
22369 [EXCERPT]
22370 a1
22371 b1
22372 [EXCERPT]
22373 ˇ[FOLDED]
22374 [EXCERPT]
22375 [FOLDED]
22376 "
22377 });
22378 cx.simulate_keystroke("up");
22379 cx.assert_excerpts_with_selections(indoc! {"
22380 [EXCERPT]
22381 [FOLDED]
22382 [EXCERPT]
22383 a1
22384 b1
22385 ˇ[EXCERPT]
22386 [FOLDED]
22387 [EXCERPT]
22388 [FOLDED]
22389 "
22390 });
22391 cx.simulate_keystroke("up");
22392 cx.assert_excerpts_with_selections(indoc! {"
22393 [EXCERPT]
22394 [FOLDED]
22395 [EXCERPT]
22396 a1
22397 ˇb1
22398 [EXCERPT]
22399 [FOLDED]
22400 [EXCERPT]
22401 [FOLDED]
22402 "
22403 });
22404 cx.simulate_keystroke("up");
22405 cx.assert_excerpts_with_selections(indoc! {"
22406 [EXCERPT]
22407 [FOLDED]
22408 [EXCERPT]
22409 ˇa1
22410 b1
22411 [EXCERPT]
22412 [FOLDED]
22413 [EXCERPT]
22414 [FOLDED]
22415 "
22416 });
22417 for _ in 0..5 {
22418 cx.simulate_keystroke("up");
22419 cx.assert_excerpts_with_selections(indoc! {"
22420 [EXCERPT]
22421 ˇ[FOLDED]
22422 [EXCERPT]
22423 a1
22424 b1
22425 [EXCERPT]
22426 [FOLDED]
22427 [EXCERPT]
22428 [FOLDED]
22429 "
22430 });
22431 }
22432}
22433
22434#[gpui::test]
22435async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22436 init_test(cx, |_| {});
22437
22438 // Simple insertion
22439 assert_highlighted_edits(
22440 "Hello, world!",
22441 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22442 true,
22443 cx,
22444 |highlighted_edits, cx| {
22445 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22446 assert_eq!(highlighted_edits.highlights.len(), 1);
22447 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22448 assert_eq!(
22449 highlighted_edits.highlights[0].1.background_color,
22450 Some(cx.theme().status().created_background)
22451 );
22452 },
22453 )
22454 .await;
22455
22456 // Replacement
22457 assert_highlighted_edits(
22458 "This is a test.",
22459 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22460 false,
22461 cx,
22462 |highlighted_edits, cx| {
22463 assert_eq!(highlighted_edits.text, "That is a test.");
22464 assert_eq!(highlighted_edits.highlights.len(), 1);
22465 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22466 assert_eq!(
22467 highlighted_edits.highlights[0].1.background_color,
22468 Some(cx.theme().status().created_background)
22469 );
22470 },
22471 )
22472 .await;
22473
22474 // Multiple edits
22475 assert_highlighted_edits(
22476 "Hello, world!",
22477 vec![
22478 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22479 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22480 ],
22481 false,
22482 cx,
22483 |highlighted_edits, cx| {
22484 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22485 assert_eq!(highlighted_edits.highlights.len(), 2);
22486 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22487 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22488 assert_eq!(
22489 highlighted_edits.highlights[0].1.background_color,
22490 Some(cx.theme().status().created_background)
22491 );
22492 assert_eq!(
22493 highlighted_edits.highlights[1].1.background_color,
22494 Some(cx.theme().status().created_background)
22495 );
22496 },
22497 )
22498 .await;
22499
22500 // Multiple lines with edits
22501 assert_highlighted_edits(
22502 "First line\nSecond line\nThird line\nFourth line",
22503 vec![
22504 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22505 (
22506 Point::new(2, 0)..Point::new(2, 10),
22507 "New third line".to_string(),
22508 ),
22509 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22510 ],
22511 false,
22512 cx,
22513 |highlighted_edits, cx| {
22514 assert_eq!(
22515 highlighted_edits.text,
22516 "Second modified\nNew third line\nFourth updated line"
22517 );
22518 assert_eq!(highlighted_edits.highlights.len(), 3);
22519 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22520 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22521 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22522 for highlight in &highlighted_edits.highlights {
22523 assert_eq!(
22524 highlight.1.background_color,
22525 Some(cx.theme().status().created_background)
22526 );
22527 }
22528 },
22529 )
22530 .await;
22531}
22532
22533#[gpui::test]
22534async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22535 init_test(cx, |_| {});
22536
22537 // Deletion
22538 assert_highlighted_edits(
22539 "Hello, world!",
22540 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22541 true,
22542 cx,
22543 |highlighted_edits, cx| {
22544 assert_eq!(highlighted_edits.text, "Hello, world!");
22545 assert_eq!(highlighted_edits.highlights.len(), 1);
22546 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22547 assert_eq!(
22548 highlighted_edits.highlights[0].1.background_color,
22549 Some(cx.theme().status().deleted_background)
22550 );
22551 },
22552 )
22553 .await;
22554
22555 // Insertion
22556 assert_highlighted_edits(
22557 "Hello, world!",
22558 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22559 true,
22560 cx,
22561 |highlighted_edits, cx| {
22562 assert_eq!(highlighted_edits.highlights.len(), 1);
22563 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22564 assert_eq!(
22565 highlighted_edits.highlights[0].1.background_color,
22566 Some(cx.theme().status().created_background)
22567 );
22568 },
22569 )
22570 .await;
22571}
22572
22573async fn assert_highlighted_edits(
22574 text: &str,
22575 edits: Vec<(Range<Point>, String)>,
22576 include_deletions: bool,
22577 cx: &mut TestAppContext,
22578 assertion_fn: impl Fn(HighlightedText, &App),
22579) {
22580 let window = cx.add_window(|window, cx| {
22581 let buffer = MultiBuffer::build_simple(text, cx);
22582 Editor::new(EditorMode::full(), buffer, None, window, cx)
22583 });
22584 let cx = &mut VisualTestContext::from_window(*window, cx);
22585
22586 let (buffer, snapshot) = window
22587 .update(cx, |editor, _window, cx| {
22588 (
22589 editor.buffer().clone(),
22590 editor.buffer().read(cx).snapshot(cx),
22591 )
22592 })
22593 .unwrap();
22594
22595 let edits = edits
22596 .into_iter()
22597 .map(|(range, edit)| {
22598 (
22599 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22600 edit,
22601 )
22602 })
22603 .collect::<Vec<_>>();
22604
22605 let text_anchor_edits = edits
22606 .clone()
22607 .into_iter()
22608 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22609 .collect::<Vec<_>>();
22610
22611 let edit_preview = window
22612 .update(cx, |_, _window, cx| {
22613 buffer
22614 .read(cx)
22615 .as_singleton()
22616 .unwrap()
22617 .read(cx)
22618 .preview_edits(text_anchor_edits.into(), cx)
22619 })
22620 .unwrap()
22621 .await;
22622
22623 cx.update(|_window, cx| {
22624 let highlighted_edits = edit_prediction_edit_text(
22625 snapshot.as_singleton().unwrap().2,
22626 &edits,
22627 &edit_preview,
22628 include_deletions,
22629 cx,
22630 );
22631 assertion_fn(highlighted_edits, cx)
22632 });
22633}
22634
22635#[track_caller]
22636fn assert_breakpoint(
22637 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22638 path: &Arc<Path>,
22639 expected: Vec<(u32, Breakpoint)>,
22640) {
22641 if expected.is_empty() {
22642 assert!(!breakpoints.contains_key(path), "{}", path.display());
22643 } else {
22644 let mut breakpoint = breakpoints
22645 .get(path)
22646 .unwrap()
22647 .iter()
22648 .map(|breakpoint| {
22649 (
22650 breakpoint.row,
22651 Breakpoint {
22652 message: breakpoint.message.clone(),
22653 state: breakpoint.state,
22654 condition: breakpoint.condition.clone(),
22655 hit_condition: breakpoint.hit_condition.clone(),
22656 },
22657 )
22658 })
22659 .collect::<Vec<_>>();
22660
22661 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22662
22663 assert_eq!(expected, breakpoint);
22664 }
22665}
22666
22667fn add_log_breakpoint_at_cursor(
22668 editor: &mut Editor,
22669 log_message: &str,
22670 window: &mut Window,
22671 cx: &mut Context<Editor>,
22672) {
22673 let (anchor, bp) = editor
22674 .breakpoints_at_cursors(window, cx)
22675 .first()
22676 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22677 .unwrap_or_else(|| {
22678 let cursor_position: Point = editor.selections.newest(cx).head();
22679
22680 let breakpoint_position = editor
22681 .snapshot(window, cx)
22682 .display_snapshot
22683 .buffer_snapshot()
22684 .anchor_before(Point::new(cursor_position.row, 0));
22685
22686 (breakpoint_position, Breakpoint::new_log(log_message))
22687 });
22688
22689 editor.edit_breakpoint_at_anchor(
22690 anchor,
22691 bp,
22692 BreakpointEditAction::EditLogMessage(log_message.into()),
22693 cx,
22694 );
22695}
22696
22697#[gpui::test]
22698async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22699 init_test(cx, |_| {});
22700
22701 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22702 let fs = FakeFs::new(cx.executor());
22703 fs.insert_tree(
22704 path!("/a"),
22705 json!({
22706 "main.rs": sample_text,
22707 }),
22708 )
22709 .await;
22710 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22711 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22712 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22713
22714 let fs = FakeFs::new(cx.executor());
22715 fs.insert_tree(
22716 path!("/a"),
22717 json!({
22718 "main.rs": sample_text,
22719 }),
22720 )
22721 .await;
22722 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22723 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22724 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22725 let worktree_id = workspace
22726 .update(cx, |workspace, _window, cx| {
22727 workspace.project().update(cx, |project, cx| {
22728 project.worktrees(cx).next().unwrap().read(cx).id()
22729 })
22730 })
22731 .unwrap();
22732
22733 let buffer = project
22734 .update(cx, |project, cx| {
22735 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22736 })
22737 .await
22738 .unwrap();
22739
22740 let (editor, cx) = cx.add_window_view(|window, cx| {
22741 Editor::new(
22742 EditorMode::full(),
22743 MultiBuffer::build_from_buffer(buffer, cx),
22744 Some(project.clone()),
22745 window,
22746 cx,
22747 )
22748 });
22749
22750 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22751 let abs_path = project.read_with(cx, |project, cx| {
22752 project
22753 .absolute_path(&project_path, cx)
22754 .map(Arc::from)
22755 .unwrap()
22756 });
22757
22758 // assert we can add breakpoint on the first line
22759 editor.update_in(cx, |editor, window, cx| {
22760 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22761 editor.move_to_end(&MoveToEnd, window, cx);
22762 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22763 });
22764
22765 let breakpoints = editor.update(cx, |editor, cx| {
22766 editor
22767 .breakpoint_store()
22768 .as_ref()
22769 .unwrap()
22770 .read(cx)
22771 .all_source_breakpoints(cx)
22772 });
22773
22774 assert_eq!(1, breakpoints.len());
22775 assert_breakpoint(
22776 &breakpoints,
22777 &abs_path,
22778 vec![
22779 (0, Breakpoint::new_standard()),
22780 (3, Breakpoint::new_standard()),
22781 ],
22782 );
22783
22784 editor.update_in(cx, |editor, window, cx| {
22785 editor.move_to_beginning(&MoveToBeginning, window, cx);
22786 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22787 });
22788
22789 let breakpoints = editor.update(cx, |editor, cx| {
22790 editor
22791 .breakpoint_store()
22792 .as_ref()
22793 .unwrap()
22794 .read(cx)
22795 .all_source_breakpoints(cx)
22796 });
22797
22798 assert_eq!(1, breakpoints.len());
22799 assert_breakpoint(
22800 &breakpoints,
22801 &abs_path,
22802 vec![(3, Breakpoint::new_standard())],
22803 );
22804
22805 editor.update_in(cx, |editor, window, cx| {
22806 editor.move_to_end(&MoveToEnd, window, cx);
22807 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22808 });
22809
22810 let breakpoints = editor.update(cx, |editor, cx| {
22811 editor
22812 .breakpoint_store()
22813 .as_ref()
22814 .unwrap()
22815 .read(cx)
22816 .all_source_breakpoints(cx)
22817 });
22818
22819 assert_eq!(0, breakpoints.len());
22820 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22821}
22822
22823#[gpui::test]
22824async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22825 init_test(cx, |_| {});
22826
22827 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22828
22829 let fs = FakeFs::new(cx.executor());
22830 fs.insert_tree(
22831 path!("/a"),
22832 json!({
22833 "main.rs": sample_text,
22834 }),
22835 )
22836 .await;
22837 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22838 let (workspace, cx) =
22839 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22840
22841 let worktree_id = workspace.update(cx, |workspace, cx| {
22842 workspace.project().update(cx, |project, cx| {
22843 project.worktrees(cx).next().unwrap().read(cx).id()
22844 })
22845 });
22846
22847 let buffer = project
22848 .update(cx, |project, cx| {
22849 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22850 })
22851 .await
22852 .unwrap();
22853
22854 let (editor, cx) = cx.add_window_view(|window, cx| {
22855 Editor::new(
22856 EditorMode::full(),
22857 MultiBuffer::build_from_buffer(buffer, cx),
22858 Some(project.clone()),
22859 window,
22860 cx,
22861 )
22862 });
22863
22864 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22865 let abs_path = project.read_with(cx, |project, cx| {
22866 project
22867 .absolute_path(&project_path, cx)
22868 .map(Arc::from)
22869 .unwrap()
22870 });
22871
22872 editor.update_in(cx, |editor, window, cx| {
22873 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22874 });
22875
22876 let breakpoints = editor.update(cx, |editor, cx| {
22877 editor
22878 .breakpoint_store()
22879 .as_ref()
22880 .unwrap()
22881 .read(cx)
22882 .all_source_breakpoints(cx)
22883 });
22884
22885 assert_breakpoint(
22886 &breakpoints,
22887 &abs_path,
22888 vec![(0, Breakpoint::new_log("hello world"))],
22889 );
22890
22891 // Removing a log message from a log breakpoint should remove it
22892 editor.update_in(cx, |editor, window, cx| {
22893 add_log_breakpoint_at_cursor(editor, "", window, cx);
22894 });
22895
22896 let breakpoints = editor.update(cx, |editor, cx| {
22897 editor
22898 .breakpoint_store()
22899 .as_ref()
22900 .unwrap()
22901 .read(cx)
22902 .all_source_breakpoints(cx)
22903 });
22904
22905 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22906
22907 editor.update_in(cx, |editor, window, cx| {
22908 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22909 editor.move_to_end(&MoveToEnd, window, cx);
22910 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22911 // Not adding a log message to a standard breakpoint shouldn't remove it
22912 add_log_breakpoint_at_cursor(editor, "", window, cx);
22913 });
22914
22915 let breakpoints = editor.update(cx, |editor, cx| {
22916 editor
22917 .breakpoint_store()
22918 .as_ref()
22919 .unwrap()
22920 .read(cx)
22921 .all_source_breakpoints(cx)
22922 });
22923
22924 assert_breakpoint(
22925 &breakpoints,
22926 &abs_path,
22927 vec![
22928 (0, Breakpoint::new_standard()),
22929 (3, Breakpoint::new_standard()),
22930 ],
22931 );
22932
22933 editor.update_in(cx, |editor, window, cx| {
22934 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22935 });
22936
22937 let breakpoints = editor.update(cx, |editor, cx| {
22938 editor
22939 .breakpoint_store()
22940 .as_ref()
22941 .unwrap()
22942 .read(cx)
22943 .all_source_breakpoints(cx)
22944 });
22945
22946 assert_breakpoint(
22947 &breakpoints,
22948 &abs_path,
22949 vec![
22950 (0, Breakpoint::new_standard()),
22951 (3, Breakpoint::new_log("hello world")),
22952 ],
22953 );
22954
22955 editor.update_in(cx, |editor, window, cx| {
22956 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22957 });
22958
22959 let breakpoints = editor.update(cx, |editor, cx| {
22960 editor
22961 .breakpoint_store()
22962 .as_ref()
22963 .unwrap()
22964 .read(cx)
22965 .all_source_breakpoints(cx)
22966 });
22967
22968 assert_breakpoint(
22969 &breakpoints,
22970 &abs_path,
22971 vec![
22972 (0, Breakpoint::new_standard()),
22973 (3, Breakpoint::new_log("hello Earth!!")),
22974 ],
22975 );
22976}
22977
22978/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22979/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22980/// or when breakpoints were placed out of order. This tests for a regression too
22981#[gpui::test]
22982async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22983 init_test(cx, |_| {});
22984
22985 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22986 let fs = FakeFs::new(cx.executor());
22987 fs.insert_tree(
22988 path!("/a"),
22989 json!({
22990 "main.rs": sample_text,
22991 }),
22992 )
22993 .await;
22994 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22995 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22996 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22997
22998 let fs = FakeFs::new(cx.executor());
22999 fs.insert_tree(
23000 path!("/a"),
23001 json!({
23002 "main.rs": sample_text,
23003 }),
23004 )
23005 .await;
23006 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23007 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23008 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23009 let worktree_id = workspace
23010 .update(cx, |workspace, _window, cx| {
23011 workspace.project().update(cx, |project, cx| {
23012 project.worktrees(cx).next().unwrap().read(cx).id()
23013 })
23014 })
23015 .unwrap();
23016
23017 let buffer = project
23018 .update(cx, |project, cx| {
23019 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23020 })
23021 .await
23022 .unwrap();
23023
23024 let (editor, cx) = cx.add_window_view(|window, cx| {
23025 Editor::new(
23026 EditorMode::full(),
23027 MultiBuffer::build_from_buffer(buffer, cx),
23028 Some(project.clone()),
23029 window,
23030 cx,
23031 )
23032 });
23033
23034 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23035 let abs_path = project.read_with(cx, |project, cx| {
23036 project
23037 .absolute_path(&project_path, cx)
23038 .map(Arc::from)
23039 .unwrap()
23040 });
23041
23042 // assert we can add breakpoint on the first line
23043 editor.update_in(cx, |editor, window, cx| {
23044 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23045 editor.move_to_end(&MoveToEnd, window, cx);
23046 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23047 editor.move_up(&MoveUp, window, cx);
23048 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23049 });
23050
23051 let breakpoints = editor.update(cx, |editor, cx| {
23052 editor
23053 .breakpoint_store()
23054 .as_ref()
23055 .unwrap()
23056 .read(cx)
23057 .all_source_breakpoints(cx)
23058 });
23059
23060 assert_eq!(1, breakpoints.len());
23061 assert_breakpoint(
23062 &breakpoints,
23063 &abs_path,
23064 vec![
23065 (0, Breakpoint::new_standard()),
23066 (2, Breakpoint::new_standard()),
23067 (3, Breakpoint::new_standard()),
23068 ],
23069 );
23070
23071 editor.update_in(cx, |editor, window, cx| {
23072 editor.move_to_beginning(&MoveToBeginning, window, cx);
23073 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23074 editor.move_to_end(&MoveToEnd, window, cx);
23075 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23076 // Disabling a breakpoint that doesn't exist should do nothing
23077 editor.move_up(&MoveUp, window, cx);
23078 editor.move_up(&MoveUp, window, cx);
23079 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23080 });
23081
23082 let breakpoints = editor.update(cx, |editor, cx| {
23083 editor
23084 .breakpoint_store()
23085 .as_ref()
23086 .unwrap()
23087 .read(cx)
23088 .all_source_breakpoints(cx)
23089 });
23090
23091 let disable_breakpoint = {
23092 let mut bp = Breakpoint::new_standard();
23093 bp.state = BreakpointState::Disabled;
23094 bp
23095 };
23096
23097 assert_eq!(1, breakpoints.len());
23098 assert_breakpoint(
23099 &breakpoints,
23100 &abs_path,
23101 vec![
23102 (0, disable_breakpoint.clone()),
23103 (2, Breakpoint::new_standard()),
23104 (3, disable_breakpoint.clone()),
23105 ],
23106 );
23107
23108 editor.update_in(cx, |editor, window, cx| {
23109 editor.move_to_beginning(&MoveToBeginning, window, cx);
23110 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23111 editor.move_to_end(&MoveToEnd, window, cx);
23112 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23113 editor.move_up(&MoveUp, window, cx);
23114 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23115 });
23116
23117 let breakpoints = editor.update(cx, |editor, cx| {
23118 editor
23119 .breakpoint_store()
23120 .as_ref()
23121 .unwrap()
23122 .read(cx)
23123 .all_source_breakpoints(cx)
23124 });
23125
23126 assert_eq!(1, breakpoints.len());
23127 assert_breakpoint(
23128 &breakpoints,
23129 &abs_path,
23130 vec![
23131 (0, Breakpoint::new_standard()),
23132 (2, disable_breakpoint),
23133 (3, Breakpoint::new_standard()),
23134 ],
23135 );
23136}
23137
23138#[gpui::test]
23139async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23140 init_test(cx, |_| {});
23141 let capabilities = lsp::ServerCapabilities {
23142 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23143 prepare_provider: Some(true),
23144 work_done_progress_options: Default::default(),
23145 })),
23146 ..Default::default()
23147 };
23148 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23149
23150 cx.set_state(indoc! {"
23151 struct Fˇoo {}
23152 "});
23153
23154 cx.update_editor(|editor, _, cx| {
23155 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23156 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23157 editor.highlight_background::<DocumentHighlightRead>(
23158 &[highlight_range],
23159 |theme| theme.colors().editor_document_highlight_read_background,
23160 cx,
23161 );
23162 });
23163
23164 let mut prepare_rename_handler = cx
23165 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23166 move |_, _, _| async move {
23167 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23168 start: lsp::Position {
23169 line: 0,
23170 character: 7,
23171 },
23172 end: lsp::Position {
23173 line: 0,
23174 character: 10,
23175 },
23176 })))
23177 },
23178 );
23179 let prepare_rename_task = cx
23180 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23181 .expect("Prepare rename was not started");
23182 prepare_rename_handler.next().await.unwrap();
23183 prepare_rename_task.await.expect("Prepare rename failed");
23184
23185 let mut rename_handler =
23186 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23187 let edit = lsp::TextEdit {
23188 range: lsp::Range {
23189 start: lsp::Position {
23190 line: 0,
23191 character: 7,
23192 },
23193 end: lsp::Position {
23194 line: 0,
23195 character: 10,
23196 },
23197 },
23198 new_text: "FooRenamed".to_string(),
23199 };
23200 Ok(Some(lsp::WorkspaceEdit::new(
23201 // Specify the same edit twice
23202 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23203 )))
23204 });
23205 let rename_task = cx
23206 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23207 .expect("Confirm rename was not started");
23208 rename_handler.next().await.unwrap();
23209 rename_task.await.expect("Confirm rename failed");
23210 cx.run_until_parked();
23211
23212 // Despite two edits, only one is actually applied as those are identical
23213 cx.assert_editor_state(indoc! {"
23214 struct FooRenamedˇ {}
23215 "});
23216}
23217
23218#[gpui::test]
23219async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23220 init_test(cx, |_| {});
23221 // These capabilities indicate that the server does not support prepare rename.
23222 let capabilities = lsp::ServerCapabilities {
23223 rename_provider: Some(lsp::OneOf::Left(true)),
23224 ..Default::default()
23225 };
23226 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23227
23228 cx.set_state(indoc! {"
23229 struct Fˇoo {}
23230 "});
23231
23232 cx.update_editor(|editor, _window, cx| {
23233 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23234 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23235 editor.highlight_background::<DocumentHighlightRead>(
23236 &[highlight_range],
23237 |theme| theme.colors().editor_document_highlight_read_background,
23238 cx,
23239 );
23240 });
23241
23242 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23243 .expect("Prepare rename was not started")
23244 .await
23245 .expect("Prepare rename failed");
23246
23247 let mut rename_handler =
23248 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23249 let edit = lsp::TextEdit {
23250 range: lsp::Range {
23251 start: lsp::Position {
23252 line: 0,
23253 character: 7,
23254 },
23255 end: lsp::Position {
23256 line: 0,
23257 character: 10,
23258 },
23259 },
23260 new_text: "FooRenamed".to_string(),
23261 };
23262 Ok(Some(lsp::WorkspaceEdit::new(
23263 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23264 )))
23265 });
23266 let rename_task = cx
23267 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23268 .expect("Confirm rename was not started");
23269 rename_handler.next().await.unwrap();
23270 rename_task.await.expect("Confirm rename failed");
23271 cx.run_until_parked();
23272
23273 // Correct range is renamed, as `surrounding_word` is used to find it.
23274 cx.assert_editor_state(indoc! {"
23275 struct FooRenamedˇ {}
23276 "});
23277}
23278
23279#[gpui::test]
23280async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23281 init_test(cx, |_| {});
23282 let mut cx = EditorTestContext::new(cx).await;
23283
23284 let language = Arc::new(
23285 Language::new(
23286 LanguageConfig::default(),
23287 Some(tree_sitter_html::LANGUAGE.into()),
23288 )
23289 .with_brackets_query(
23290 r#"
23291 ("<" @open "/>" @close)
23292 ("</" @open ">" @close)
23293 ("<" @open ">" @close)
23294 ("\"" @open "\"" @close)
23295 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23296 "#,
23297 )
23298 .unwrap(),
23299 );
23300 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23301
23302 cx.set_state(indoc! {"
23303 <span>ˇ</span>
23304 "});
23305 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23306 cx.assert_editor_state(indoc! {"
23307 <span>
23308 ˇ
23309 </span>
23310 "});
23311
23312 cx.set_state(indoc! {"
23313 <span><span></span>ˇ</span>
23314 "});
23315 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23316 cx.assert_editor_state(indoc! {"
23317 <span><span></span>
23318 ˇ</span>
23319 "});
23320
23321 cx.set_state(indoc! {"
23322 <span>ˇ
23323 </span>
23324 "});
23325 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23326 cx.assert_editor_state(indoc! {"
23327 <span>
23328 ˇ
23329 </span>
23330 "});
23331}
23332
23333#[gpui::test(iterations = 10)]
23334async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23335 init_test(cx, |_| {});
23336
23337 let fs = FakeFs::new(cx.executor());
23338 fs.insert_tree(
23339 path!("/dir"),
23340 json!({
23341 "a.ts": "a",
23342 }),
23343 )
23344 .await;
23345
23346 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23347 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23348 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23349
23350 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23351 language_registry.add(Arc::new(Language::new(
23352 LanguageConfig {
23353 name: "TypeScript".into(),
23354 matcher: LanguageMatcher {
23355 path_suffixes: vec!["ts".to_string()],
23356 ..Default::default()
23357 },
23358 ..Default::default()
23359 },
23360 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23361 )));
23362 let mut fake_language_servers = language_registry.register_fake_lsp(
23363 "TypeScript",
23364 FakeLspAdapter {
23365 capabilities: lsp::ServerCapabilities {
23366 code_lens_provider: Some(lsp::CodeLensOptions {
23367 resolve_provider: Some(true),
23368 }),
23369 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23370 commands: vec!["_the/command".to_string()],
23371 ..lsp::ExecuteCommandOptions::default()
23372 }),
23373 ..lsp::ServerCapabilities::default()
23374 },
23375 ..FakeLspAdapter::default()
23376 },
23377 );
23378
23379 let editor = workspace
23380 .update(cx, |workspace, window, cx| {
23381 workspace.open_abs_path(
23382 PathBuf::from(path!("/dir/a.ts")),
23383 OpenOptions::default(),
23384 window,
23385 cx,
23386 )
23387 })
23388 .unwrap()
23389 .await
23390 .unwrap()
23391 .downcast::<Editor>()
23392 .unwrap();
23393 cx.executor().run_until_parked();
23394
23395 let fake_server = fake_language_servers.next().await.unwrap();
23396
23397 let buffer = editor.update(cx, |editor, cx| {
23398 editor
23399 .buffer()
23400 .read(cx)
23401 .as_singleton()
23402 .expect("have opened a single file by path")
23403 });
23404
23405 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23406 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23407 drop(buffer_snapshot);
23408 let actions = cx
23409 .update_window(*workspace, |_, window, cx| {
23410 project.code_actions(&buffer, anchor..anchor, window, cx)
23411 })
23412 .unwrap();
23413
23414 fake_server
23415 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23416 Ok(Some(vec![
23417 lsp::CodeLens {
23418 range: lsp::Range::default(),
23419 command: Some(lsp::Command {
23420 title: "Code lens command".to_owned(),
23421 command: "_the/command".to_owned(),
23422 arguments: None,
23423 }),
23424 data: None,
23425 },
23426 lsp::CodeLens {
23427 range: lsp::Range::default(),
23428 command: Some(lsp::Command {
23429 title: "Command not in capabilities".to_owned(),
23430 command: "not in capabilities".to_owned(),
23431 arguments: None,
23432 }),
23433 data: None,
23434 },
23435 lsp::CodeLens {
23436 range: lsp::Range {
23437 start: lsp::Position {
23438 line: 1,
23439 character: 1,
23440 },
23441 end: lsp::Position {
23442 line: 1,
23443 character: 1,
23444 },
23445 },
23446 command: Some(lsp::Command {
23447 title: "Command not in range".to_owned(),
23448 command: "_the/command".to_owned(),
23449 arguments: None,
23450 }),
23451 data: None,
23452 },
23453 ]))
23454 })
23455 .next()
23456 .await;
23457
23458 let actions = actions.await.unwrap();
23459 assert_eq!(
23460 actions.len(),
23461 1,
23462 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23463 );
23464 let action = actions[0].clone();
23465 let apply = project.update(cx, |project, cx| {
23466 project.apply_code_action(buffer.clone(), action, true, cx)
23467 });
23468
23469 // Resolving the code action does not populate its edits. In absence of
23470 // edits, we must execute the given command.
23471 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23472 |mut lens, _| async move {
23473 let lens_command = lens.command.as_mut().expect("should have a command");
23474 assert_eq!(lens_command.title, "Code lens command");
23475 lens_command.arguments = Some(vec![json!("the-argument")]);
23476 Ok(lens)
23477 },
23478 );
23479
23480 // While executing the command, the language server sends the editor
23481 // a `workspaceEdit` request.
23482 fake_server
23483 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23484 let fake = fake_server.clone();
23485 move |params, _| {
23486 assert_eq!(params.command, "_the/command");
23487 let fake = fake.clone();
23488 async move {
23489 fake.server
23490 .request::<lsp::request::ApplyWorkspaceEdit>(
23491 lsp::ApplyWorkspaceEditParams {
23492 label: None,
23493 edit: lsp::WorkspaceEdit {
23494 changes: Some(
23495 [(
23496 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23497 vec![lsp::TextEdit {
23498 range: lsp::Range::new(
23499 lsp::Position::new(0, 0),
23500 lsp::Position::new(0, 0),
23501 ),
23502 new_text: "X".into(),
23503 }],
23504 )]
23505 .into_iter()
23506 .collect(),
23507 ),
23508 ..lsp::WorkspaceEdit::default()
23509 },
23510 },
23511 )
23512 .await
23513 .into_response()
23514 .unwrap();
23515 Ok(Some(json!(null)))
23516 }
23517 }
23518 })
23519 .next()
23520 .await;
23521
23522 // Applying the code lens command returns a project transaction containing the edits
23523 // sent by the language server in its `workspaceEdit` request.
23524 let transaction = apply.await.unwrap();
23525 assert!(transaction.0.contains_key(&buffer));
23526 buffer.update(cx, |buffer, cx| {
23527 assert_eq!(buffer.text(), "Xa");
23528 buffer.undo(cx);
23529 assert_eq!(buffer.text(), "a");
23530 });
23531
23532 let actions_after_edits = cx
23533 .update_window(*workspace, |_, window, cx| {
23534 project.code_actions(&buffer, anchor..anchor, window, cx)
23535 })
23536 .unwrap()
23537 .await
23538 .unwrap();
23539 assert_eq!(
23540 actions, actions_after_edits,
23541 "For the same selection, same code lens actions should be returned"
23542 );
23543
23544 let _responses =
23545 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23546 panic!("No more code lens requests are expected");
23547 });
23548 editor.update_in(cx, |editor, window, cx| {
23549 editor.select_all(&SelectAll, window, cx);
23550 });
23551 cx.executor().run_until_parked();
23552 let new_actions = cx
23553 .update_window(*workspace, |_, window, cx| {
23554 project.code_actions(&buffer, anchor..anchor, window, cx)
23555 })
23556 .unwrap()
23557 .await
23558 .unwrap();
23559 assert_eq!(
23560 actions, new_actions,
23561 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23562 );
23563}
23564
23565#[gpui::test]
23566async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23567 init_test(cx, |_| {});
23568
23569 let fs = FakeFs::new(cx.executor());
23570 let main_text = r#"fn main() {
23571println!("1");
23572println!("2");
23573println!("3");
23574println!("4");
23575println!("5");
23576}"#;
23577 let lib_text = "mod foo {}";
23578 fs.insert_tree(
23579 path!("/a"),
23580 json!({
23581 "lib.rs": lib_text,
23582 "main.rs": main_text,
23583 }),
23584 )
23585 .await;
23586
23587 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23588 let (workspace, cx) =
23589 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23590 let worktree_id = workspace.update(cx, |workspace, cx| {
23591 workspace.project().update(cx, |project, cx| {
23592 project.worktrees(cx).next().unwrap().read(cx).id()
23593 })
23594 });
23595
23596 let expected_ranges = vec![
23597 Point::new(0, 0)..Point::new(0, 0),
23598 Point::new(1, 0)..Point::new(1, 1),
23599 Point::new(2, 0)..Point::new(2, 2),
23600 Point::new(3, 0)..Point::new(3, 3),
23601 ];
23602
23603 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23604 let editor_1 = workspace
23605 .update_in(cx, |workspace, window, cx| {
23606 workspace.open_path(
23607 (worktree_id, rel_path("main.rs")),
23608 Some(pane_1.downgrade()),
23609 true,
23610 window,
23611 cx,
23612 )
23613 })
23614 .unwrap()
23615 .await
23616 .downcast::<Editor>()
23617 .unwrap();
23618 pane_1.update(cx, |pane, cx| {
23619 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23620 open_editor.update(cx, |editor, cx| {
23621 assert_eq!(
23622 editor.display_text(cx),
23623 main_text,
23624 "Original main.rs text on initial open",
23625 );
23626 assert_eq!(
23627 editor
23628 .selections
23629 .all::<Point>(cx)
23630 .into_iter()
23631 .map(|s| s.range())
23632 .collect::<Vec<_>>(),
23633 vec![Point::zero()..Point::zero()],
23634 "Default selections on initial open",
23635 );
23636 })
23637 });
23638 editor_1.update_in(cx, |editor, window, cx| {
23639 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23640 s.select_ranges(expected_ranges.clone());
23641 });
23642 });
23643
23644 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23645 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23646 });
23647 let editor_2 = workspace
23648 .update_in(cx, |workspace, window, cx| {
23649 workspace.open_path(
23650 (worktree_id, rel_path("main.rs")),
23651 Some(pane_2.downgrade()),
23652 true,
23653 window,
23654 cx,
23655 )
23656 })
23657 .unwrap()
23658 .await
23659 .downcast::<Editor>()
23660 .unwrap();
23661 pane_2.update(cx, |pane, cx| {
23662 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23663 open_editor.update(cx, |editor, cx| {
23664 assert_eq!(
23665 editor.display_text(cx),
23666 main_text,
23667 "Original main.rs text on initial open in another panel",
23668 );
23669 assert_eq!(
23670 editor
23671 .selections
23672 .all::<Point>(cx)
23673 .into_iter()
23674 .map(|s| s.range())
23675 .collect::<Vec<_>>(),
23676 vec![Point::zero()..Point::zero()],
23677 "Default selections on initial open in another panel",
23678 );
23679 })
23680 });
23681
23682 editor_2.update_in(cx, |editor, window, cx| {
23683 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23684 });
23685
23686 let _other_editor_1 = workspace
23687 .update_in(cx, |workspace, window, cx| {
23688 workspace.open_path(
23689 (worktree_id, rel_path("lib.rs")),
23690 Some(pane_1.downgrade()),
23691 true,
23692 window,
23693 cx,
23694 )
23695 })
23696 .unwrap()
23697 .await
23698 .downcast::<Editor>()
23699 .unwrap();
23700 pane_1
23701 .update_in(cx, |pane, window, cx| {
23702 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23703 })
23704 .await
23705 .unwrap();
23706 drop(editor_1);
23707 pane_1.update(cx, |pane, cx| {
23708 pane.active_item()
23709 .unwrap()
23710 .downcast::<Editor>()
23711 .unwrap()
23712 .update(cx, |editor, cx| {
23713 assert_eq!(
23714 editor.display_text(cx),
23715 lib_text,
23716 "Other file should be open and active",
23717 );
23718 });
23719 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23720 });
23721
23722 let _other_editor_2 = workspace
23723 .update_in(cx, |workspace, window, cx| {
23724 workspace.open_path(
23725 (worktree_id, rel_path("lib.rs")),
23726 Some(pane_2.downgrade()),
23727 true,
23728 window,
23729 cx,
23730 )
23731 })
23732 .unwrap()
23733 .await
23734 .downcast::<Editor>()
23735 .unwrap();
23736 pane_2
23737 .update_in(cx, |pane, window, cx| {
23738 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23739 })
23740 .await
23741 .unwrap();
23742 drop(editor_2);
23743 pane_2.update(cx, |pane, cx| {
23744 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23745 open_editor.update(cx, |editor, cx| {
23746 assert_eq!(
23747 editor.display_text(cx),
23748 lib_text,
23749 "Other file should be open and active in another panel too",
23750 );
23751 });
23752 assert_eq!(
23753 pane.items().count(),
23754 1,
23755 "No other editors should be open in another pane",
23756 );
23757 });
23758
23759 let _editor_1_reopened = workspace
23760 .update_in(cx, |workspace, window, cx| {
23761 workspace.open_path(
23762 (worktree_id, rel_path("main.rs")),
23763 Some(pane_1.downgrade()),
23764 true,
23765 window,
23766 cx,
23767 )
23768 })
23769 .unwrap()
23770 .await
23771 .downcast::<Editor>()
23772 .unwrap();
23773 let _editor_2_reopened = workspace
23774 .update_in(cx, |workspace, window, cx| {
23775 workspace.open_path(
23776 (worktree_id, rel_path("main.rs")),
23777 Some(pane_2.downgrade()),
23778 true,
23779 window,
23780 cx,
23781 )
23782 })
23783 .unwrap()
23784 .await
23785 .downcast::<Editor>()
23786 .unwrap();
23787 pane_1.update(cx, |pane, cx| {
23788 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23789 open_editor.update(cx, |editor, cx| {
23790 assert_eq!(
23791 editor.display_text(cx),
23792 main_text,
23793 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23794 );
23795 assert_eq!(
23796 editor
23797 .selections
23798 .all::<Point>(cx)
23799 .into_iter()
23800 .map(|s| s.range())
23801 .collect::<Vec<_>>(),
23802 expected_ranges,
23803 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23804 );
23805 })
23806 });
23807 pane_2.update(cx, |pane, cx| {
23808 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23809 open_editor.update(cx, |editor, cx| {
23810 assert_eq!(
23811 editor.display_text(cx),
23812 r#"fn main() {
23813⋯rintln!("1");
23814⋯intln!("2");
23815⋯ntln!("3");
23816println!("4");
23817println!("5");
23818}"#,
23819 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23820 );
23821 assert_eq!(
23822 editor
23823 .selections
23824 .all::<Point>(cx)
23825 .into_iter()
23826 .map(|s| s.range())
23827 .collect::<Vec<_>>(),
23828 vec![Point::zero()..Point::zero()],
23829 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23830 );
23831 })
23832 });
23833}
23834
23835#[gpui::test]
23836async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23837 init_test(cx, |_| {});
23838
23839 let fs = FakeFs::new(cx.executor());
23840 let main_text = r#"fn main() {
23841println!("1");
23842println!("2");
23843println!("3");
23844println!("4");
23845println!("5");
23846}"#;
23847 let lib_text = "mod foo {}";
23848 fs.insert_tree(
23849 path!("/a"),
23850 json!({
23851 "lib.rs": lib_text,
23852 "main.rs": main_text,
23853 }),
23854 )
23855 .await;
23856
23857 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23858 let (workspace, cx) =
23859 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23860 let worktree_id = workspace.update(cx, |workspace, cx| {
23861 workspace.project().update(cx, |project, cx| {
23862 project.worktrees(cx).next().unwrap().read(cx).id()
23863 })
23864 });
23865
23866 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23867 let editor = workspace
23868 .update_in(cx, |workspace, window, cx| {
23869 workspace.open_path(
23870 (worktree_id, rel_path("main.rs")),
23871 Some(pane.downgrade()),
23872 true,
23873 window,
23874 cx,
23875 )
23876 })
23877 .unwrap()
23878 .await
23879 .downcast::<Editor>()
23880 .unwrap();
23881 pane.update(cx, |pane, cx| {
23882 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23883 open_editor.update(cx, |editor, cx| {
23884 assert_eq!(
23885 editor.display_text(cx),
23886 main_text,
23887 "Original main.rs text on initial open",
23888 );
23889 })
23890 });
23891 editor.update_in(cx, |editor, window, cx| {
23892 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23893 });
23894
23895 cx.update_global(|store: &mut SettingsStore, cx| {
23896 store.update_user_settings(cx, |s| {
23897 s.workspace.restore_on_file_reopen = Some(false);
23898 });
23899 });
23900 editor.update_in(cx, |editor, window, cx| {
23901 editor.fold_ranges(
23902 vec![
23903 Point::new(1, 0)..Point::new(1, 1),
23904 Point::new(2, 0)..Point::new(2, 2),
23905 Point::new(3, 0)..Point::new(3, 3),
23906 ],
23907 false,
23908 window,
23909 cx,
23910 );
23911 });
23912 pane.update_in(cx, |pane, window, cx| {
23913 pane.close_all_items(&CloseAllItems::default(), window, cx)
23914 })
23915 .await
23916 .unwrap();
23917 pane.update(cx, |pane, _| {
23918 assert!(pane.active_item().is_none());
23919 });
23920 cx.update_global(|store: &mut SettingsStore, cx| {
23921 store.update_user_settings(cx, |s| {
23922 s.workspace.restore_on_file_reopen = Some(true);
23923 });
23924 });
23925
23926 let _editor_reopened = workspace
23927 .update_in(cx, |workspace, window, cx| {
23928 workspace.open_path(
23929 (worktree_id, rel_path("main.rs")),
23930 Some(pane.downgrade()),
23931 true,
23932 window,
23933 cx,
23934 )
23935 })
23936 .unwrap()
23937 .await
23938 .downcast::<Editor>()
23939 .unwrap();
23940 pane.update(cx, |pane, cx| {
23941 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23942 open_editor.update(cx, |editor, cx| {
23943 assert_eq!(
23944 editor.display_text(cx),
23945 main_text,
23946 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23947 );
23948 })
23949 });
23950}
23951
23952#[gpui::test]
23953async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23954 struct EmptyModalView {
23955 focus_handle: gpui::FocusHandle,
23956 }
23957 impl EventEmitter<DismissEvent> for EmptyModalView {}
23958 impl Render for EmptyModalView {
23959 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23960 div()
23961 }
23962 }
23963 impl Focusable for EmptyModalView {
23964 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23965 self.focus_handle.clone()
23966 }
23967 }
23968 impl workspace::ModalView for EmptyModalView {}
23969 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23970 EmptyModalView {
23971 focus_handle: cx.focus_handle(),
23972 }
23973 }
23974
23975 init_test(cx, |_| {});
23976
23977 let fs = FakeFs::new(cx.executor());
23978 let project = Project::test(fs, [], cx).await;
23979 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23980 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23981 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23982 let editor = cx.new_window_entity(|window, cx| {
23983 Editor::new(
23984 EditorMode::full(),
23985 buffer,
23986 Some(project.clone()),
23987 window,
23988 cx,
23989 )
23990 });
23991 workspace
23992 .update(cx, |workspace, window, cx| {
23993 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23994 })
23995 .unwrap();
23996 editor.update_in(cx, |editor, window, cx| {
23997 editor.open_context_menu(&OpenContextMenu, window, cx);
23998 assert!(editor.mouse_context_menu.is_some());
23999 });
24000 workspace
24001 .update(cx, |workspace, window, cx| {
24002 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24003 })
24004 .unwrap();
24005 cx.read(|cx| {
24006 assert!(editor.read(cx).mouse_context_menu.is_none());
24007 });
24008}
24009
24010fn set_linked_edit_ranges(
24011 opening: (Point, Point),
24012 closing: (Point, Point),
24013 editor: &mut Editor,
24014 cx: &mut Context<Editor>,
24015) {
24016 let Some((buffer, _)) = editor
24017 .buffer
24018 .read(cx)
24019 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24020 else {
24021 panic!("Failed to get buffer for selection position");
24022 };
24023 let buffer = buffer.read(cx);
24024 let buffer_id = buffer.remote_id();
24025 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24026 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24027 let mut linked_ranges = HashMap::default();
24028 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24029 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24030}
24031
24032#[gpui::test]
24033async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24034 init_test(cx, |_| {});
24035
24036 let fs = FakeFs::new(cx.executor());
24037 fs.insert_file(path!("/file.html"), Default::default())
24038 .await;
24039
24040 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24041
24042 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24043 let html_language = Arc::new(Language::new(
24044 LanguageConfig {
24045 name: "HTML".into(),
24046 matcher: LanguageMatcher {
24047 path_suffixes: vec!["html".to_string()],
24048 ..LanguageMatcher::default()
24049 },
24050 brackets: BracketPairConfig {
24051 pairs: vec![BracketPair {
24052 start: "<".into(),
24053 end: ">".into(),
24054 close: true,
24055 ..Default::default()
24056 }],
24057 ..Default::default()
24058 },
24059 ..Default::default()
24060 },
24061 Some(tree_sitter_html::LANGUAGE.into()),
24062 ));
24063 language_registry.add(html_language);
24064 let mut fake_servers = language_registry.register_fake_lsp(
24065 "HTML",
24066 FakeLspAdapter {
24067 capabilities: lsp::ServerCapabilities {
24068 completion_provider: Some(lsp::CompletionOptions {
24069 resolve_provider: Some(true),
24070 ..Default::default()
24071 }),
24072 ..Default::default()
24073 },
24074 ..Default::default()
24075 },
24076 );
24077
24078 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24079 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24080
24081 let worktree_id = workspace
24082 .update(cx, |workspace, _window, cx| {
24083 workspace.project().update(cx, |project, cx| {
24084 project.worktrees(cx).next().unwrap().read(cx).id()
24085 })
24086 })
24087 .unwrap();
24088 project
24089 .update(cx, |project, cx| {
24090 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24091 })
24092 .await
24093 .unwrap();
24094 let editor = workspace
24095 .update(cx, |workspace, window, cx| {
24096 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24097 })
24098 .unwrap()
24099 .await
24100 .unwrap()
24101 .downcast::<Editor>()
24102 .unwrap();
24103
24104 let fake_server = fake_servers.next().await.unwrap();
24105 editor.update_in(cx, |editor, window, cx| {
24106 editor.set_text("<ad></ad>", window, cx);
24107 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24108 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24109 });
24110 set_linked_edit_ranges(
24111 (Point::new(0, 1), Point::new(0, 3)),
24112 (Point::new(0, 6), Point::new(0, 8)),
24113 editor,
24114 cx,
24115 );
24116 });
24117 let mut completion_handle =
24118 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24119 Ok(Some(lsp::CompletionResponse::Array(vec![
24120 lsp::CompletionItem {
24121 label: "head".to_string(),
24122 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24123 lsp::InsertReplaceEdit {
24124 new_text: "head".to_string(),
24125 insert: lsp::Range::new(
24126 lsp::Position::new(0, 1),
24127 lsp::Position::new(0, 3),
24128 ),
24129 replace: lsp::Range::new(
24130 lsp::Position::new(0, 1),
24131 lsp::Position::new(0, 3),
24132 ),
24133 },
24134 )),
24135 ..Default::default()
24136 },
24137 ])))
24138 });
24139 editor.update_in(cx, |editor, window, cx| {
24140 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
24141 });
24142 cx.run_until_parked();
24143 completion_handle.next().await.unwrap();
24144 editor.update(cx, |editor, _| {
24145 assert!(
24146 editor.context_menu_visible(),
24147 "Completion menu should be visible"
24148 );
24149 });
24150 editor.update_in(cx, |editor, window, cx| {
24151 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24152 });
24153 cx.executor().run_until_parked();
24154 editor.update(cx, |editor, cx| {
24155 assert_eq!(editor.text(cx), "<head></head>");
24156 });
24157}
24158
24159#[gpui::test]
24160async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24161 init_test(cx, |_| {});
24162
24163 let mut cx = EditorTestContext::new(cx).await;
24164 let language = Arc::new(Language::new(
24165 LanguageConfig {
24166 name: "TSX".into(),
24167 matcher: LanguageMatcher {
24168 path_suffixes: vec!["tsx".to_string()],
24169 ..LanguageMatcher::default()
24170 },
24171 brackets: BracketPairConfig {
24172 pairs: vec![BracketPair {
24173 start: "<".into(),
24174 end: ">".into(),
24175 close: true,
24176 ..Default::default()
24177 }],
24178 ..Default::default()
24179 },
24180 linked_edit_characters: HashSet::from_iter(['.']),
24181 ..Default::default()
24182 },
24183 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24184 ));
24185 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24186
24187 // Test typing > does not extend linked pair
24188 cx.set_state("<divˇ<div></div>");
24189 cx.update_editor(|editor, _, cx| {
24190 set_linked_edit_ranges(
24191 (Point::new(0, 1), Point::new(0, 4)),
24192 (Point::new(0, 11), Point::new(0, 14)),
24193 editor,
24194 cx,
24195 );
24196 });
24197 cx.update_editor(|editor, window, cx| {
24198 editor.handle_input(">", window, cx);
24199 });
24200 cx.assert_editor_state("<div>ˇ<div></div>");
24201
24202 // Test typing . do extend linked pair
24203 cx.set_state("<Animatedˇ></Animated>");
24204 cx.update_editor(|editor, _, cx| {
24205 set_linked_edit_ranges(
24206 (Point::new(0, 1), Point::new(0, 9)),
24207 (Point::new(0, 12), Point::new(0, 20)),
24208 editor,
24209 cx,
24210 );
24211 });
24212 cx.update_editor(|editor, window, cx| {
24213 editor.handle_input(".", window, cx);
24214 });
24215 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24216 cx.update_editor(|editor, _, cx| {
24217 set_linked_edit_ranges(
24218 (Point::new(0, 1), Point::new(0, 10)),
24219 (Point::new(0, 13), Point::new(0, 21)),
24220 editor,
24221 cx,
24222 );
24223 });
24224 cx.update_editor(|editor, window, cx| {
24225 editor.handle_input("V", window, cx);
24226 });
24227 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24228}
24229
24230#[gpui::test]
24231async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24232 init_test(cx, |_| {});
24233
24234 let fs = FakeFs::new(cx.executor());
24235 fs.insert_tree(
24236 path!("/root"),
24237 json!({
24238 "a": {
24239 "main.rs": "fn main() {}",
24240 },
24241 "foo": {
24242 "bar": {
24243 "external_file.rs": "pub mod external {}",
24244 }
24245 }
24246 }),
24247 )
24248 .await;
24249
24250 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24251 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24252 language_registry.add(rust_lang());
24253 let _fake_servers = language_registry.register_fake_lsp(
24254 "Rust",
24255 FakeLspAdapter {
24256 ..FakeLspAdapter::default()
24257 },
24258 );
24259 let (workspace, cx) =
24260 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24261 let worktree_id = workspace.update(cx, |workspace, cx| {
24262 workspace.project().update(cx, |project, cx| {
24263 project.worktrees(cx).next().unwrap().read(cx).id()
24264 })
24265 });
24266
24267 let assert_language_servers_count =
24268 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24269 project.update(cx, |project, cx| {
24270 let current = project
24271 .lsp_store()
24272 .read(cx)
24273 .as_local()
24274 .unwrap()
24275 .language_servers
24276 .len();
24277 assert_eq!(expected, current, "{context}");
24278 });
24279 };
24280
24281 assert_language_servers_count(
24282 0,
24283 "No servers should be running before any file is open",
24284 cx,
24285 );
24286 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24287 let main_editor = workspace
24288 .update_in(cx, |workspace, window, cx| {
24289 workspace.open_path(
24290 (worktree_id, rel_path("main.rs")),
24291 Some(pane.downgrade()),
24292 true,
24293 window,
24294 cx,
24295 )
24296 })
24297 .unwrap()
24298 .await
24299 .downcast::<Editor>()
24300 .unwrap();
24301 pane.update(cx, |pane, cx| {
24302 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24303 open_editor.update(cx, |editor, cx| {
24304 assert_eq!(
24305 editor.display_text(cx),
24306 "fn main() {}",
24307 "Original main.rs text on initial open",
24308 );
24309 });
24310 assert_eq!(open_editor, main_editor);
24311 });
24312 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24313
24314 let external_editor = workspace
24315 .update_in(cx, |workspace, window, cx| {
24316 workspace.open_abs_path(
24317 PathBuf::from("/root/foo/bar/external_file.rs"),
24318 OpenOptions::default(),
24319 window,
24320 cx,
24321 )
24322 })
24323 .await
24324 .expect("opening external file")
24325 .downcast::<Editor>()
24326 .expect("downcasted external file's open element to editor");
24327 pane.update(cx, |pane, cx| {
24328 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24329 open_editor.update(cx, |editor, cx| {
24330 assert_eq!(
24331 editor.display_text(cx),
24332 "pub mod external {}",
24333 "External file is open now",
24334 );
24335 });
24336 assert_eq!(open_editor, external_editor);
24337 });
24338 assert_language_servers_count(
24339 1,
24340 "Second, external, *.rs file should join the existing server",
24341 cx,
24342 );
24343
24344 pane.update_in(cx, |pane, window, cx| {
24345 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24346 })
24347 .await
24348 .unwrap();
24349 pane.update_in(cx, |pane, window, cx| {
24350 pane.navigate_backward(&Default::default(), window, cx);
24351 });
24352 cx.run_until_parked();
24353 pane.update(cx, |pane, cx| {
24354 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24355 open_editor.update(cx, |editor, cx| {
24356 assert_eq!(
24357 editor.display_text(cx),
24358 "pub mod external {}",
24359 "External file is open now",
24360 );
24361 });
24362 });
24363 assert_language_servers_count(
24364 1,
24365 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24366 cx,
24367 );
24368
24369 cx.update(|_, cx| {
24370 workspace::reload(cx);
24371 });
24372 assert_language_servers_count(
24373 1,
24374 "After reloading the worktree with local and external files opened, only one project should be started",
24375 cx,
24376 );
24377}
24378
24379#[gpui::test]
24380async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24381 init_test(cx, |_| {});
24382
24383 let mut cx = EditorTestContext::new(cx).await;
24384 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24385 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24386
24387 // test cursor move to start of each line on tab
24388 // for `if`, `elif`, `else`, `while`, `with` and `for`
24389 cx.set_state(indoc! {"
24390 def main():
24391 ˇ for item in items:
24392 ˇ while item.active:
24393 ˇ if item.value > 10:
24394 ˇ continue
24395 ˇ elif item.value < 0:
24396 ˇ break
24397 ˇ else:
24398 ˇ with item.context() as ctx:
24399 ˇ yield count
24400 ˇ else:
24401 ˇ log('while else')
24402 ˇ else:
24403 ˇ log('for else')
24404 "});
24405 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24406 cx.assert_editor_state(indoc! {"
24407 def main():
24408 ˇfor item in items:
24409 ˇwhile item.active:
24410 ˇif item.value > 10:
24411 ˇcontinue
24412 ˇelif item.value < 0:
24413 ˇbreak
24414 ˇelse:
24415 ˇwith item.context() as ctx:
24416 ˇyield count
24417 ˇelse:
24418 ˇlog('while else')
24419 ˇelse:
24420 ˇlog('for else')
24421 "});
24422 // test relative indent is preserved when tab
24423 // for `if`, `elif`, `else`, `while`, `with` and `for`
24424 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24425 cx.assert_editor_state(indoc! {"
24426 def main():
24427 ˇfor item in items:
24428 ˇwhile item.active:
24429 ˇif item.value > 10:
24430 ˇcontinue
24431 ˇelif item.value < 0:
24432 ˇbreak
24433 ˇelse:
24434 ˇwith item.context() as ctx:
24435 ˇyield count
24436 ˇelse:
24437 ˇlog('while else')
24438 ˇelse:
24439 ˇlog('for else')
24440 "});
24441
24442 // test cursor move to start of each line on tab
24443 // for `try`, `except`, `else`, `finally`, `match` and `def`
24444 cx.set_state(indoc! {"
24445 def main():
24446 ˇ try:
24447 ˇ fetch()
24448 ˇ except ValueError:
24449 ˇ handle_error()
24450 ˇ else:
24451 ˇ match value:
24452 ˇ case _:
24453 ˇ finally:
24454 ˇ def status():
24455 ˇ return 0
24456 "});
24457 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24458 cx.assert_editor_state(indoc! {"
24459 def main():
24460 ˇtry:
24461 ˇfetch()
24462 ˇexcept ValueError:
24463 ˇhandle_error()
24464 ˇelse:
24465 ˇmatch value:
24466 ˇcase _:
24467 ˇfinally:
24468 ˇdef status():
24469 ˇreturn 0
24470 "});
24471 // test relative indent is preserved when tab
24472 // for `try`, `except`, `else`, `finally`, `match` and `def`
24473 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24474 cx.assert_editor_state(indoc! {"
24475 def main():
24476 ˇtry:
24477 ˇfetch()
24478 ˇexcept ValueError:
24479 ˇhandle_error()
24480 ˇelse:
24481 ˇmatch value:
24482 ˇcase _:
24483 ˇfinally:
24484 ˇdef status():
24485 ˇreturn 0
24486 "});
24487}
24488
24489#[gpui::test]
24490async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24491 init_test(cx, |_| {});
24492
24493 let mut cx = EditorTestContext::new(cx).await;
24494 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24495 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24496
24497 // test `else` auto outdents when typed inside `if` block
24498 cx.set_state(indoc! {"
24499 def main():
24500 if i == 2:
24501 return
24502 ˇ
24503 "});
24504 cx.update_editor(|editor, window, cx| {
24505 editor.handle_input("else:", window, cx);
24506 });
24507 cx.assert_editor_state(indoc! {"
24508 def main():
24509 if i == 2:
24510 return
24511 else:ˇ
24512 "});
24513
24514 // test `except` auto outdents when typed inside `try` block
24515 cx.set_state(indoc! {"
24516 def main():
24517 try:
24518 i = 2
24519 ˇ
24520 "});
24521 cx.update_editor(|editor, window, cx| {
24522 editor.handle_input("except:", window, cx);
24523 });
24524 cx.assert_editor_state(indoc! {"
24525 def main():
24526 try:
24527 i = 2
24528 except:ˇ
24529 "});
24530
24531 // test `else` auto outdents when typed inside `except` block
24532 cx.set_state(indoc! {"
24533 def main():
24534 try:
24535 i = 2
24536 except:
24537 j = 2
24538 ˇ
24539 "});
24540 cx.update_editor(|editor, window, cx| {
24541 editor.handle_input("else:", window, cx);
24542 });
24543 cx.assert_editor_state(indoc! {"
24544 def main():
24545 try:
24546 i = 2
24547 except:
24548 j = 2
24549 else:ˇ
24550 "});
24551
24552 // test `finally` auto outdents when typed inside `else` block
24553 cx.set_state(indoc! {"
24554 def main():
24555 try:
24556 i = 2
24557 except:
24558 j = 2
24559 else:
24560 k = 2
24561 ˇ
24562 "});
24563 cx.update_editor(|editor, window, cx| {
24564 editor.handle_input("finally:", window, cx);
24565 });
24566 cx.assert_editor_state(indoc! {"
24567 def main():
24568 try:
24569 i = 2
24570 except:
24571 j = 2
24572 else:
24573 k = 2
24574 finally:ˇ
24575 "});
24576
24577 // test `else` does not outdents when typed inside `except` block right after for block
24578 cx.set_state(indoc! {"
24579 def main():
24580 try:
24581 i = 2
24582 except:
24583 for i in range(n):
24584 pass
24585 ˇ
24586 "});
24587 cx.update_editor(|editor, window, cx| {
24588 editor.handle_input("else:", window, cx);
24589 });
24590 cx.assert_editor_state(indoc! {"
24591 def main():
24592 try:
24593 i = 2
24594 except:
24595 for i in range(n):
24596 pass
24597 else:ˇ
24598 "});
24599
24600 // test `finally` auto outdents when typed inside `else` block right after for block
24601 cx.set_state(indoc! {"
24602 def main():
24603 try:
24604 i = 2
24605 except:
24606 j = 2
24607 else:
24608 for i in range(n):
24609 pass
24610 ˇ
24611 "});
24612 cx.update_editor(|editor, window, cx| {
24613 editor.handle_input("finally:", window, cx);
24614 });
24615 cx.assert_editor_state(indoc! {"
24616 def main():
24617 try:
24618 i = 2
24619 except:
24620 j = 2
24621 else:
24622 for i in range(n):
24623 pass
24624 finally:ˇ
24625 "});
24626
24627 // test `except` outdents to inner "try" block
24628 cx.set_state(indoc! {"
24629 def main():
24630 try:
24631 i = 2
24632 if i == 2:
24633 try:
24634 i = 3
24635 ˇ
24636 "});
24637 cx.update_editor(|editor, window, cx| {
24638 editor.handle_input("except:", window, cx);
24639 });
24640 cx.assert_editor_state(indoc! {"
24641 def main():
24642 try:
24643 i = 2
24644 if i == 2:
24645 try:
24646 i = 3
24647 except:ˇ
24648 "});
24649
24650 // test `except` outdents to outer "try" block
24651 cx.set_state(indoc! {"
24652 def main():
24653 try:
24654 i = 2
24655 if i == 2:
24656 try:
24657 i = 3
24658 ˇ
24659 "});
24660 cx.update_editor(|editor, window, cx| {
24661 editor.handle_input("except:", window, cx);
24662 });
24663 cx.assert_editor_state(indoc! {"
24664 def main():
24665 try:
24666 i = 2
24667 if i == 2:
24668 try:
24669 i = 3
24670 except:ˇ
24671 "});
24672
24673 // test `else` stays at correct indent when typed after `for` block
24674 cx.set_state(indoc! {"
24675 def main():
24676 for i in range(10):
24677 if i == 3:
24678 break
24679 ˇ
24680 "});
24681 cx.update_editor(|editor, window, cx| {
24682 editor.handle_input("else:", window, cx);
24683 });
24684 cx.assert_editor_state(indoc! {"
24685 def main():
24686 for i in range(10):
24687 if i == 3:
24688 break
24689 else:ˇ
24690 "});
24691
24692 // test does not outdent on typing after line with square brackets
24693 cx.set_state(indoc! {"
24694 def f() -> list[str]:
24695 ˇ
24696 "});
24697 cx.update_editor(|editor, window, cx| {
24698 editor.handle_input("a", window, cx);
24699 });
24700 cx.assert_editor_state(indoc! {"
24701 def f() -> list[str]:
24702 aˇ
24703 "});
24704
24705 // test does not outdent on typing : after case keyword
24706 cx.set_state(indoc! {"
24707 match 1:
24708 caseˇ
24709 "});
24710 cx.update_editor(|editor, window, cx| {
24711 editor.handle_input(":", window, cx);
24712 });
24713 cx.assert_editor_state(indoc! {"
24714 match 1:
24715 case:ˇ
24716 "});
24717}
24718
24719#[gpui::test]
24720async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24721 init_test(cx, |_| {});
24722 update_test_language_settings(cx, |settings| {
24723 settings.defaults.extend_comment_on_newline = Some(false);
24724 });
24725 let mut cx = EditorTestContext::new(cx).await;
24726 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24727 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24728
24729 // test correct indent after newline on comment
24730 cx.set_state(indoc! {"
24731 # COMMENT:ˇ
24732 "});
24733 cx.update_editor(|editor, window, cx| {
24734 editor.newline(&Newline, window, cx);
24735 });
24736 cx.assert_editor_state(indoc! {"
24737 # COMMENT:
24738 ˇ
24739 "});
24740
24741 // test correct indent after newline in brackets
24742 cx.set_state(indoc! {"
24743 {ˇ}
24744 "});
24745 cx.update_editor(|editor, window, cx| {
24746 editor.newline(&Newline, window, cx);
24747 });
24748 cx.run_until_parked();
24749 cx.assert_editor_state(indoc! {"
24750 {
24751 ˇ
24752 }
24753 "});
24754
24755 cx.set_state(indoc! {"
24756 (ˇ)
24757 "});
24758 cx.update_editor(|editor, window, cx| {
24759 editor.newline(&Newline, window, cx);
24760 });
24761 cx.run_until_parked();
24762 cx.assert_editor_state(indoc! {"
24763 (
24764 ˇ
24765 )
24766 "});
24767
24768 // do not indent after empty lists or dictionaries
24769 cx.set_state(indoc! {"
24770 a = []ˇ
24771 "});
24772 cx.update_editor(|editor, window, cx| {
24773 editor.newline(&Newline, window, cx);
24774 });
24775 cx.run_until_parked();
24776 cx.assert_editor_state(indoc! {"
24777 a = []
24778 ˇ
24779 "});
24780}
24781
24782#[gpui::test]
24783async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24784 init_test(cx, |_| {});
24785
24786 let mut cx = EditorTestContext::new(cx).await;
24787 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24788 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24789
24790 // test cursor move to start of each line on tab
24791 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24792 cx.set_state(indoc! {"
24793 function main() {
24794 ˇ for item in $items; do
24795 ˇ while [ -n \"$item\" ]; do
24796 ˇ if [ \"$value\" -gt 10 ]; then
24797 ˇ continue
24798 ˇ elif [ \"$value\" -lt 0 ]; then
24799 ˇ break
24800 ˇ else
24801 ˇ echo \"$item\"
24802 ˇ fi
24803 ˇ done
24804 ˇ done
24805 ˇ}
24806 "});
24807 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24808 cx.assert_editor_state(indoc! {"
24809 function main() {
24810 ˇfor item in $items; do
24811 ˇwhile [ -n \"$item\" ]; do
24812 ˇif [ \"$value\" -gt 10 ]; then
24813 ˇcontinue
24814 ˇelif [ \"$value\" -lt 0 ]; then
24815 ˇbreak
24816 ˇelse
24817 ˇecho \"$item\"
24818 ˇfi
24819 ˇdone
24820 ˇdone
24821 ˇ}
24822 "});
24823 // test relative indent is preserved when tab
24824 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24825 cx.assert_editor_state(indoc! {"
24826 function main() {
24827 ˇfor item in $items; do
24828 ˇwhile [ -n \"$item\" ]; do
24829 ˇif [ \"$value\" -gt 10 ]; then
24830 ˇcontinue
24831 ˇelif [ \"$value\" -lt 0 ]; then
24832 ˇbreak
24833 ˇelse
24834 ˇecho \"$item\"
24835 ˇfi
24836 ˇdone
24837 ˇdone
24838 ˇ}
24839 "});
24840
24841 // test cursor move to start of each line on tab
24842 // for `case` statement with patterns
24843 cx.set_state(indoc! {"
24844 function handle() {
24845 ˇ case \"$1\" in
24846 ˇ start)
24847 ˇ echo \"a\"
24848 ˇ ;;
24849 ˇ stop)
24850 ˇ echo \"b\"
24851 ˇ ;;
24852 ˇ *)
24853 ˇ echo \"c\"
24854 ˇ ;;
24855 ˇ esac
24856 ˇ}
24857 "});
24858 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24859 cx.assert_editor_state(indoc! {"
24860 function handle() {
24861 ˇcase \"$1\" in
24862 ˇstart)
24863 ˇecho \"a\"
24864 ˇ;;
24865 ˇstop)
24866 ˇecho \"b\"
24867 ˇ;;
24868 ˇ*)
24869 ˇecho \"c\"
24870 ˇ;;
24871 ˇesac
24872 ˇ}
24873 "});
24874}
24875
24876#[gpui::test]
24877async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24878 init_test(cx, |_| {});
24879
24880 let mut cx = EditorTestContext::new(cx).await;
24881 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24882 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24883
24884 // test indents on comment insert
24885 cx.set_state(indoc! {"
24886 function main() {
24887 ˇ for item in $items; do
24888 ˇ while [ -n \"$item\" ]; do
24889 ˇ if [ \"$value\" -gt 10 ]; then
24890 ˇ continue
24891 ˇ elif [ \"$value\" -lt 0 ]; then
24892 ˇ break
24893 ˇ else
24894 ˇ echo \"$item\"
24895 ˇ fi
24896 ˇ done
24897 ˇ done
24898 ˇ}
24899 "});
24900 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24901 cx.assert_editor_state(indoc! {"
24902 function main() {
24903 #ˇ for item in $items; do
24904 #ˇ while [ -n \"$item\" ]; do
24905 #ˇ if [ \"$value\" -gt 10 ]; then
24906 #ˇ continue
24907 #ˇ elif [ \"$value\" -lt 0 ]; then
24908 #ˇ break
24909 #ˇ else
24910 #ˇ echo \"$item\"
24911 #ˇ fi
24912 #ˇ done
24913 #ˇ done
24914 #ˇ}
24915 "});
24916}
24917
24918#[gpui::test]
24919async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24920 init_test(cx, |_| {});
24921
24922 let mut cx = EditorTestContext::new(cx).await;
24923 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24924 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24925
24926 // test `else` auto outdents when typed inside `if` block
24927 cx.set_state(indoc! {"
24928 if [ \"$1\" = \"test\" ]; then
24929 echo \"foo bar\"
24930 ˇ
24931 "});
24932 cx.update_editor(|editor, window, cx| {
24933 editor.handle_input("else", window, cx);
24934 });
24935 cx.assert_editor_state(indoc! {"
24936 if [ \"$1\" = \"test\" ]; then
24937 echo \"foo bar\"
24938 elseˇ
24939 "});
24940
24941 // test `elif` auto outdents when typed inside `if` block
24942 cx.set_state(indoc! {"
24943 if [ \"$1\" = \"test\" ]; then
24944 echo \"foo bar\"
24945 ˇ
24946 "});
24947 cx.update_editor(|editor, window, cx| {
24948 editor.handle_input("elif", window, cx);
24949 });
24950 cx.assert_editor_state(indoc! {"
24951 if [ \"$1\" = \"test\" ]; then
24952 echo \"foo bar\"
24953 elifˇ
24954 "});
24955
24956 // test `fi` auto outdents when typed inside `else` block
24957 cx.set_state(indoc! {"
24958 if [ \"$1\" = \"test\" ]; then
24959 echo \"foo bar\"
24960 else
24961 echo \"bar baz\"
24962 ˇ
24963 "});
24964 cx.update_editor(|editor, window, cx| {
24965 editor.handle_input("fi", window, cx);
24966 });
24967 cx.assert_editor_state(indoc! {"
24968 if [ \"$1\" = \"test\" ]; then
24969 echo \"foo bar\"
24970 else
24971 echo \"bar baz\"
24972 fiˇ
24973 "});
24974
24975 // test `done` auto outdents when typed inside `while` block
24976 cx.set_state(indoc! {"
24977 while read line; do
24978 echo \"$line\"
24979 ˇ
24980 "});
24981 cx.update_editor(|editor, window, cx| {
24982 editor.handle_input("done", window, cx);
24983 });
24984 cx.assert_editor_state(indoc! {"
24985 while read line; do
24986 echo \"$line\"
24987 doneˇ
24988 "});
24989
24990 // test `done` auto outdents when typed inside `for` block
24991 cx.set_state(indoc! {"
24992 for file in *.txt; do
24993 cat \"$file\"
24994 ˇ
24995 "});
24996 cx.update_editor(|editor, window, cx| {
24997 editor.handle_input("done", window, cx);
24998 });
24999 cx.assert_editor_state(indoc! {"
25000 for file in *.txt; do
25001 cat \"$file\"
25002 doneˇ
25003 "});
25004
25005 // test `esac` auto outdents when typed inside `case` block
25006 cx.set_state(indoc! {"
25007 case \"$1\" in
25008 start)
25009 echo \"foo bar\"
25010 ;;
25011 stop)
25012 echo \"bar baz\"
25013 ;;
25014 ˇ
25015 "});
25016 cx.update_editor(|editor, window, cx| {
25017 editor.handle_input("esac", window, cx);
25018 });
25019 cx.assert_editor_state(indoc! {"
25020 case \"$1\" in
25021 start)
25022 echo \"foo bar\"
25023 ;;
25024 stop)
25025 echo \"bar baz\"
25026 ;;
25027 esacˇ
25028 "});
25029
25030 // test `*)` auto outdents when typed inside `case` block
25031 cx.set_state(indoc! {"
25032 case \"$1\" in
25033 start)
25034 echo \"foo bar\"
25035 ;;
25036 ˇ
25037 "});
25038 cx.update_editor(|editor, window, cx| {
25039 editor.handle_input("*)", window, cx);
25040 });
25041 cx.assert_editor_state(indoc! {"
25042 case \"$1\" in
25043 start)
25044 echo \"foo bar\"
25045 ;;
25046 *)ˇ
25047 "});
25048
25049 // test `fi` outdents to correct level with nested if blocks
25050 cx.set_state(indoc! {"
25051 if [ \"$1\" = \"test\" ]; then
25052 echo \"outer if\"
25053 if [ \"$2\" = \"debug\" ]; then
25054 echo \"inner if\"
25055 ˇ
25056 "});
25057 cx.update_editor(|editor, window, cx| {
25058 editor.handle_input("fi", window, cx);
25059 });
25060 cx.assert_editor_state(indoc! {"
25061 if [ \"$1\" = \"test\" ]; then
25062 echo \"outer if\"
25063 if [ \"$2\" = \"debug\" ]; then
25064 echo \"inner if\"
25065 fiˇ
25066 "});
25067}
25068
25069#[gpui::test]
25070async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25071 init_test(cx, |_| {});
25072 update_test_language_settings(cx, |settings| {
25073 settings.defaults.extend_comment_on_newline = Some(false);
25074 });
25075 let mut cx = EditorTestContext::new(cx).await;
25076 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25077 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25078
25079 // test correct indent after newline on comment
25080 cx.set_state(indoc! {"
25081 # COMMENT:ˇ
25082 "});
25083 cx.update_editor(|editor, window, cx| {
25084 editor.newline(&Newline, window, cx);
25085 });
25086 cx.assert_editor_state(indoc! {"
25087 # COMMENT:
25088 ˇ
25089 "});
25090
25091 // test correct indent after newline after `then`
25092 cx.set_state(indoc! {"
25093
25094 if [ \"$1\" = \"test\" ]; thenˇ
25095 "});
25096 cx.update_editor(|editor, window, cx| {
25097 editor.newline(&Newline, window, cx);
25098 });
25099 cx.run_until_parked();
25100 cx.assert_editor_state(indoc! {"
25101
25102 if [ \"$1\" = \"test\" ]; then
25103 ˇ
25104 "});
25105
25106 // test correct indent after newline after `else`
25107 cx.set_state(indoc! {"
25108 if [ \"$1\" = \"test\" ]; then
25109 elseˇ
25110 "});
25111 cx.update_editor(|editor, window, cx| {
25112 editor.newline(&Newline, window, cx);
25113 });
25114 cx.run_until_parked();
25115 cx.assert_editor_state(indoc! {"
25116 if [ \"$1\" = \"test\" ]; then
25117 else
25118 ˇ
25119 "});
25120
25121 // test correct indent after newline after `elif`
25122 cx.set_state(indoc! {"
25123 if [ \"$1\" = \"test\" ]; then
25124 elifˇ
25125 "});
25126 cx.update_editor(|editor, window, cx| {
25127 editor.newline(&Newline, window, cx);
25128 });
25129 cx.run_until_parked();
25130 cx.assert_editor_state(indoc! {"
25131 if [ \"$1\" = \"test\" ]; then
25132 elif
25133 ˇ
25134 "});
25135
25136 // test correct indent after newline after `do`
25137 cx.set_state(indoc! {"
25138 for file in *.txt; doˇ
25139 "});
25140 cx.update_editor(|editor, window, cx| {
25141 editor.newline(&Newline, window, cx);
25142 });
25143 cx.run_until_parked();
25144 cx.assert_editor_state(indoc! {"
25145 for file in *.txt; do
25146 ˇ
25147 "});
25148
25149 // test correct indent after newline after case pattern
25150 cx.set_state(indoc! {"
25151 case \"$1\" in
25152 start)ˇ
25153 "});
25154 cx.update_editor(|editor, window, cx| {
25155 editor.newline(&Newline, window, cx);
25156 });
25157 cx.run_until_parked();
25158 cx.assert_editor_state(indoc! {"
25159 case \"$1\" in
25160 start)
25161 ˇ
25162 "});
25163
25164 // test correct indent after newline after case pattern
25165 cx.set_state(indoc! {"
25166 case \"$1\" in
25167 start)
25168 ;;
25169 *)ˇ
25170 "});
25171 cx.update_editor(|editor, window, cx| {
25172 editor.newline(&Newline, window, cx);
25173 });
25174 cx.run_until_parked();
25175 cx.assert_editor_state(indoc! {"
25176 case \"$1\" in
25177 start)
25178 ;;
25179 *)
25180 ˇ
25181 "});
25182
25183 // test correct indent after newline after function opening brace
25184 cx.set_state(indoc! {"
25185 function test() {ˇ}
25186 "});
25187 cx.update_editor(|editor, window, cx| {
25188 editor.newline(&Newline, window, cx);
25189 });
25190 cx.run_until_parked();
25191 cx.assert_editor_state(indoc! {"
25192 function test() {
25193 ˇ
25194 }
25195 "});
25196
25197 // test no extra indent after semicolon on same line
25198 cx.set_state(indoc! {"
25199 echo \"test\";ˇ
25200 "});
25201 cx.update_editor(|editor, window, cx| {
25202 editor.newline(&Newline, window, cx);
25203 });
25204 cx.run_until_parked();
25205 cx.assert_editor_state(indoc! {"
25206 echo \"test\";
25207 ˇ
25208 "});
25209}
25210
25211fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25212 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25213 point..point
25214}
25215
25216#[track_caller]
25217fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25218 let (text, ranges) = marked_text_ranges(marked_text, true);
25219 assert_eq!(editor.text(cx), text);
25220 assert_eq!(
25221 editor.selections.ranges(cx),
25222 ranges,
25223 "Assert selections are {}",
25224 marked_text
25225 );
25226}
25227
25228pub fn handle_signature_help_request(
25229 cx: &mut EditorLspTestContext,
25230 mocked_response: lsp::SignatureHelp,
25231) -> impl Future<Output = ()> + use<> {
25232 let mut request =
25233 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25234 let mocked_response = mocked_response.clone();
25235 async move { Ok(Some(mocked_response)) }
25236 });
25237
25238 async move {
25239 request.next().await;
25240 }
25241}
25242
25243#[track_caller]
25244pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25245 cx.update_editor(|editor, _, _| {
25246 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25247 let entries = menu.entries.borrow();
25248 let entries = entries
25249 .iter()
25250 .map(|entry| entry.string.as_str())
25251 .collect::<Vec<_>>();
25252 assert_eq!(entries, expected);
25253 } else {
25254 panic!("Expected completions menu");
25255 }
25256 });
25257}
25258
25259/// Handle completion request passing a marked string specifying where the completion
25260/// should be triggered from using '|' character, what range should be replaced, and what completions
25261/// should be returned using '<' and '>' to delimit the range.
25262///
25263/// Also see `handle_completion_request_with_insert_and_replace`.
25264#[track_caller]
25265pub fn handle_completion_request(
25266 marked_string: &str,
25267 completions: Vec<&'static str>,
25268 is_incomplete: bool,
25269 counter: Arc<AtomicUsize>,
25270 cx: &mut EditorLspTestContext,
25271) -> impl Future<Output = ()> {
25272 let complete_from_marker: TextRangeMarker = '|'.into();
25273 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25274 let (_, mut marked_ranges) = marked_text_ranges_by(
25275 marked_string,
25276 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25277 );
25278
25279 let complete_from_position =
25280 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25281 let replace_range =
25282 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25283
25284 let mut request =
25285 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25286 let completions = completions.clone();
25287 counter.fetch_add(1, atomic::Ordering::Release);
25288 async move {
25289 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25290 assert_eq!(
25291 params.text_document_position.position,
25292 complete_from_position
25293 );
25294 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25295 is_incomplete,
25296 item_defaults: None,
25297 items: completions
25298 .iter()
25299 .map(|completion_text| lsp::CompletionItem {
25300 label: completion_text.to_string(),
25301 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25302 range: replace_range,
25303 new_text: completion_text.to_string(),
25304 })),
25305 ..Default::default()
25306 })
25307 .collect(),
25308 })))
25309 }
25310 });
25311
25312 async move {
25313 request.next().await;
25314 }
25315}
25316
25317/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25318/// given instead, which also contains an `insert` range.
25319///
25320/// This function uses markers to define ranges:
25321/// - `|` marks the cursor position
25322/// - `<>` marks the replace range
25323/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25324pub fn handle_completion_request_with_insert_and_replace(
25325 cx: &mut EditorLspTestContext,
25326 marked_string: &str,
25327 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25328 counter: Arc<AtomicUsize>,
25329) -> impl Future<Output = ()> {
25330 let complete_from_marker: TextRangeMarker = '|'.into();
25331 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25332 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25333
25334 let (_, mut marked_ranges) = marked_text_ranges_by(
25335 marked_string,
25336 vec![
25337 complete_from_marker.clone(),
25338 replace_range_marker.clone(),
25339 insert_range_marker.clone(),
25340 ],
25341 );
25342
25343 let complete_from_position =
25344 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25345 let replace_range =
25346 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25347
25348 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25349 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25350 _ => lsp::Range {
25351 start: replace_range.start,
25352 end: complete_from_position,
25353 },
25354 };
25355
25356 let mut request =
25357 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25358 let completions = completions.clone();
25359 counter.fetch_add(1, atomic::Ordering::Release);
25360 async move {
25361 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25362 assert_eq!(
25363 params.text_document_position.position, complete_from_position,
25364 "marker `|` position doesn't match",
25365 );
25366 Ok(Some(lsp::CompletionResponse::Array(
25367 completions
25368 .iter()
25369 .map(|(label, new_text)| lsp::CompletionItem {
25370 label: label.to_string(),
25371 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25372 lsp::InsertReplaceEdit {
25373 insert: insert_range,
25374 replace: replace_range,
25375 new_text: new_text.to_string(),
25376 },
25377 )),
25378 ..Default::default()
25379 })
25380 .collect(),
25381 )))
25382 }
25383 });
25384
25385 async move {
25386 request.next().await;
25387 }
25388}
25389
25390fn handle_resolve_completion_request(
25391 cx: &mut EditorLspTestContext,
25392 edits: Option<Vec<(&'static str, &'static str)>>,
25393) -> impl Future<Output = ()> {
25394 let edits = edits.map(|edits| {
25395 edits
25396 .iter()
25397 .map(|(marked_string, new_text)| {
25398 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25399 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25400 lsp::TextEdit::new(replace_range, new_text.to_string())
25401 })
25402 .collect::<Vec<_>>()
25403 });
25404
25405 let mut request =
25406 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25407 let edits = edits.clone();
25408 async move {
25409 Ok(lsp::CompletionItem {
25410 additional_text_edits: edits,
25411 ..Default::default()
25412 })
25413 }
25414 });
25415
25416 async move {
25417 request.next().await;
25418 }
25419}
25420
25421pub(crate) fn update_test_language_settings(
25422 cx: &mut TestAppContext,
25423 f: impl Fn(&mut AllLanguageSettingsContent),
25424) {
25425 cx.update(|cx| {
25426 SettingsStore::update_global(cx, |store, cx| {
25427 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25428 });
25429 });
25430}
25431
25432pub(crate) fn update_test_project_settings(
25433 cx: &mut TestAppContext,
25434 f: impl Fn(&mut ProjectSettingsContent),
25435) {
25436 cx.update(|cx| {
25437 SettingsStore::update_global(cx, |store, cx| {
25438 store.update_user_settings(cx, |settings| f(&mut settings.project));
25439 });
25440 });
25441}
25442
25443pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25444 cx.update(|cx| {
25445 assets::Assets.load_test_fonts(cx);
25446 let store = SettingsStore::test(cx);
25447 cx.set_global(store);
25448 theme::init(theme::LoadThemes::JustBase, cx);
25449 release_channel::init(SemanticVersion::default(), cx);
25450 client::init_settings(cx);
25451 language::init(cx);
25452 Project::init_settings(cx);
25453 workspace::init_settings(cx);
25454 crate::init(cx);
25455 });
25456 zlog::init_test();
25457 update_test_language_settings(cx, f);
25458}
25459
25460#[track_caller]
25461fn assert_hunk_revert(
25462 not_reverted_text_with_selections: &str,
25463 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25464 expected_reverted_text_with_selections: &str,
25465 base_text: &str,
25466 cx: &mut EditorLspTestContext,
25467) {
25468 cx.set_state(not_reverted_text_with_selections);
25469 cx.set_head_text(base_text);
25470 cx.executor().run_until_parked();
25471
25472 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25473 let snapshot = editor.snapshot(window, cx);
25474 let reverted_hunk_statuses = snapshot
25475 .buffer_snapshot()
25476 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
25477 .map(|hunk| hunk.status().kind)
25478 .collect::<Vec<_>>();
25479
25480 editor.git_restore(&Default::default(), window, cx);
25481 reverted_hunk_statuses
25482 });
25483 cx.executor().run_until_parked();
25484 cx.assert_editor_state(expected_reverted_text_with_selections);
25485 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25486}
25487
25488#[gpui::test(iterations = 10)]
25489async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25490 init_test(cx, |_| {});
25491
25492 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25493 let counter = diagnostic_requests.clone();
25494
25495 let fs = FakeFs::new(cx.executor());
25496 fs.insert_tree(
25497 path!("/a"),
25498 json!({
25499 "first.rs": "fn main() { let a = 5; }",
25500 "second.rs": "// Test file",
25501 }),
25502 )
25503 .await;
25504
25505 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25506 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25507 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25508
25509 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25510 language_registry.add(rust_lang());
25511 let mut fake_servers = language_registry.register_fake_lsp(
25512 "Rust",
25513 FakeLspAdapter {
25514 capabilities: lsp::ServerCapabilities {
25515 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25516 lsp::DiagnosticOptions {
25517 identifier: None,
25518 inter_file_dependencies: true,
25519 workspace_diagnostics: true,
25520 work_done_progress_options: Default::default(),
25521 },
25522 )),
25523 ..Default::default()
25524 },
25525 ..Default::default()
25526 },
25527 );
25528
25529 let editor = workspace
25530 .update(cx, |workspace, window, cx| {
25531 workspace.open_abs_path(
25532 PathBuf::from(path!("/a/first.rs")),
25533 OpenOptions::default(),
25534 window,
25535 cx,
25536 )
25537 })
25538 .unwrap()
25539 .await
25540 .unwrap()
25541 .downcast::<Editor>()
25542 .unwrap();
25543 let fake_server = fake_servers.next().await.unwrap();
25544 let server_id = fake_server.server.server_id();
25545 let mut first_request = fake_server
25546 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25547 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25548 let result_id = Some(new_result_id.to_string());
25549 assert_eq!(
25550 params.text_document.uri,
25551 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25552 );
25553 async move {
25554 Ok(lsp::DocumentDiagnosticReportResult::Report(
25555 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25556 related_documents: None,
25557 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25558 items: Vec::new(),
25559 result_id,
25560 },
25561 }),
25562 ))
25563 }
25564 });
25565
25566 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25567 project.update(cx, |project, cx| {
25568 let buffer_id = editor
25569 .read(cx)
25570 .buffer()
25571 .read(cx)
25572 .as_singleton()
25573 .expect("created a singleton buffer")
25574 .read(cx)
25575 .remote_id();
25576 let buffer_result_id = project
25577 .lsp_store()
25578 .read(cx)
25579 .result_id(server_id, buffer_id, cx);
25580 assert_eq!(expected, buffer_result_id);
25581 });
25582 };
25583
25584 ensure_result_id(None, cx);
25585 cx.executor().advance_clock(Duration::from_millis(60));
25586 cx.executor().run_until_parked();
25587 assert_eq!(
25588 diagnostic_requests.load(atomic::Ordering::Acquire),
25589 1,
25590 "Opening file should trigger diagnostic request"
25591 );
25592 first_request
25593 .next()
25594 .await
25595 .expect("should have sent the first diagnostics pull request");
25596 ensure_result_id(Some("1".to_string()), cx);
25597
25598 // Editing should trigger diagnostics
25599 editor.update_in(cx, |editor, window, cx| {
25600 editor.handle_input("2", window, cx)
25601 });
25602 cx.executor().advance_clock(Duration::from_millis(60));
25603 cx.executor().run_until_parked();
25604 assert_eq!(
25605 diagnostic_requests.load(atomic::Ordering::Acquire),
25606 2,
25607 "Editing should trigger diagnostic request"
25608 );
25609 ensure_result_id(Some("2".to_string()), cx);
25610
25611 // Moving cursor should not trigger diagnostic request
25612 editor.update_in(cx, |editor, window, cx| {
25613 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25614 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25615 });
25616 });
25617 cx.executor().advance_clock(Duration::from_millis(60));
25618 cx.executor().run_until_parked();
25619 assert_eq!(
25620 diagnostic_requests.load(atomic::Ordering::Acquire),
25621 2,
25622 "Cursor movement should not trigger diagnostic request"
25623 );
25624 ensure_result_id(Some("2".to_string()), cx);
25625 // Multiple rapid edits should be debounced
25626 for _ in 0..5 {
25627 editor.update_in(cx, |editor, window, cx| {
25628 editor.handle_input("x", window, cx)
25629 });
25630 }
25631 cx.executor().advance_clock(Duration::from_millis(60));
25632 cx.executor().run_until_parked();
25633
25634 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25635 assert!(
25636 final_requests <= 4,
25637 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25638 );
25639 ensure_result_id(Some(final_requests.to_string()), cx);
25640}
25641
25642#[gpui::test]
25643async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25644 // Regression test for issue #11671
25645 // Previously, adding a cursor after moving multiple cursors would reset
25646 // the cursor count instead of adding to the existing cursors.
25647 init_test(cx, |_| {});
25648 let mut cx = EditorTestContext::new(cx).await;
25649
25650 // Create a simple buffer with cursor at start
25651 cx.set_state(indoc! {"
25652 ˇaaaa
25653 bbbb
25654 cccc
25655 dddd
25656 eeee
25657 ffff
25658 gggg
25659 hhhh"});
25660
25661 // Add 2 cursors below (so we have 3 total)
25662 cx.update_editor(|editor, window, cx| {
25663 editor.add_selection_below(&Default::default(), window, cx);
25664 editor.add_selection_below(&Default::default(), window, cx);
25665 });
25666
25667 // Verify we have 3 cursors
25668 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25669 assert_eq!(
25670 initial_count, 3,
25671 "Should have 3 cursors after adding 2 below"
25672 );
25673
25674 // Move down one line
25675 cx.update_editor(|editor, window, cx| {
25676 editor.move_down(&MoveDown, window, cx);
25677 });
25678
25679 // Add another cursor below
25680 cx.update_editor(|editor, window, cx| {
25681 editor.add_selection_below(&Default::default(), window, cx);
25682 });
25683
25684 // Should now have 4 cursors (3 original + 1 new)
25685 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25686 assert_eq!(
25687 final_count, 4,
25688 "Should have 4 cursors after moving and adding another"
25689 );
25690}
25691
25692#[gpui::test(iterations = 10)]
25693async fn test_document_colors(cx: &mut TestAppContext) {
25694 let expected_color = Rgba {
25695 r: 0.33,
25696 g: 0.33,
25697 b: 0.33,
25698 a: 0.33,
25699 };
25700
25701 init_test(cx, |_| {});
25702
25703 let fs = FakeFs::new(cx.executor());
25704 fs.insert_tree(
25705 path!("/a"),
25706 json!({
25707 "first.rs": "fn main() { let a = 5; }",
25708 }),
25709 )
25710 .await;
25711
25712 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25713 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25714 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25715
25716 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25717 language_registry.add(rust_lang());
25718 let mut fake_servers = language_registry.register_fake_lsp(
25719 "Rust",
25720 FakeLspAdapter {
25721 capabilities: lsp::ServerCapabilities {
25722 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25723 ..lsp::ServerCapabilities::default()
25724 },
25725 name: "rust-analyzer",
25726 ..FakeLspAdapter::default()
25727 },
25728 );
25729 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25730 "Rust",
25731 FakeLspAdapter {
25732 capabilities: lsp::ServerCapabilities {
25733 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25734 ..lsp::ServerCapabilities::default()
25735 },
25736 name: "not-rust-analyzer",
25737 ..FakeLspAdapter::default()
25738 },
25739 );
25740
25741 let editor = workspace
25742 .update(cx, |workspace, window, cx| {
25743 workspace.open_abs_path(
25744 PathBuf::from(path!("/a/first.rs")),
25745 OpenOptions::default(),
25746 window,
25747 cx,
25748 )
25749 })
25750 .unwrap()
25751 .await
25752 .unwrap()
25753 .downcast::<Editor>()
25754 .unwrap();
25755 let fake_language_server = fake_servers.next().await.unwrap();
25756 let fake_language_server_without_capabilities =
25757 fake_servers_without_capabilities.next().await.unwrap();
25758 let requests_made = Arc::new(AtomicUsize::new(0));
25759 let closure_requests_made = Arc::clone(&requests_made);
25760 let mut color_request_handle = fake_language_server
25761 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25762 let requests_made = Arc::clone(&closure_requests_made);
25763 async move {
25764 assert_eq!(
25765 params.text_document.uri,
25766 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25767 );
25768 requests_made.fetch_add(1, atomic::Ordering::Release);
25769 Ok(vec![
25770 lsp::ColorInformation {
25771 range: lsp::Range {
25772 start: lsp::Position {
25773 line: 0,
25774 character: 0,
25775 },
25776 end: lsp::Position {
25777 line: 0,
25778 character: 1,
25779 },
25780 },
25781 color: lsp::Color {
25782 red: 0.33,
25783 green: 0.33,
25784 blue: 0.33,
25785 alpha: 0.33,
25786 },
25787 },
25788 lsp::ColorInformation {
25789 range: lsp::Range {
25790 start: lsp::Position {
25791 line: 0,
25792 character: 0,
25793 },
25794 end: lsp::Position {
25795 line: 0,
25796 character: 1,
25797 },
25798 },
25799 color: lsp::Color {
25800 red: 0.33,
25801 green: 0.33,
25802 blue: 0.33,
25803 alpha: 0.33,
25804 },
25805 },
25806 ])
25807 }
25808 });
25809
25810 let _handle = fake_language_server_without_capabilities
25811 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25812 panic!("Should not be called");
25813 });
25814 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
25815 color_request_handle.next().await.unwrap();
25816 cx.run_until_parked();
25817 assert_eq!(
25818 1,
25819 requests_made.load(atomic::Ordering::Acquire),
25820 "Should query for colors once per editor open"
25821 );
25822 editor.update_in(cx, |editor, _, cx| {
25823 assert_eq!(
25824 vec![expected_color],
25825 extract_color_inlays(editor, cx),
25826 "Should have an initial inlay"
25827 );
25828 });
25829
25830 // opening another file in a split should not influence the LSP query counter
25831 workspace
25832 .update(cx, |workspace, window, cx| {
25833 assert_eq!(
25834 workspace.panes().len(),
25835 1,
25836 "Should have one pane with one editor"
25837 );
25838 workspace.move_item_to_pane_in_direction(
25839 &MoveItemToPaneInDirection {
25840 direction: SplitDirection::Right,
25841 focus: false,
25842 clone: true,
25843 },
25844 window,
25845 cx,
25846 );
25847 })
25848 .unwrap();
25849 cx.run_until_parked();
25850 workspace
25851 .update(cx, |workspace, _, cx| {
25852 let panes = workspace.panes();
25853 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25854 for pane in panes {
25855 let editor = pane
25856 .read(cx)
25857 .active_item()
25858 .and_then(|item| item.downcast::<Editor>())
25859 .expect("Should have opened an editor in each split");
25860 let editor_file = editor
25861 .read(cx)
25862 .buffer()
25863 .read(cx)
25864 .as_singleton()
25865 .expect("test deals with singleton buffers")
25866 .read(cx)
25867 .file()
25868 .expect("test buffese should have a file")
25869 .path();
25870 assert_eq!(
25871 editor_file.as_ref(),
25872 rel_path("first.rs"),
25873 "Both editors should be opened for the same file"
25874 )
25875 }
25876 })
25877 .unwrap();
25878
25879 cx.executor().advance_clock(Duration::from_millis(500));
25880 let save = editor.update_in(cx, |editor, window, cx| {
25881 editor.move_to_end(&MoveToEnd, window, cx);
25882 editor.handle_input("dirty", window, cx);
25883 editor.save(
25884 SaveOptions {
25885 format: true,
25886 autosave: true,
25887 },
25888 project.clone(),
25889 window,
25890 cx,
25891 )
25892 });
25893 save.await.unwrap();
25894
25895 color_request_handle.next().await.unwrap();
25896 cx.run_until_parked();
25897 assert_eq!(
25898 2,
25899 requests_made.load(atomic::Ordering::Acquire),
25900 "Should query for colors once per save (deduplicated) and once per formatting after save"
25901 );
25902
25903 drop(editor);
25904 let close = workspace
25905 .update(cx, |workspace, window, cx| {
25906 workspace.active_pane().update(cx, |pane, cx| {
25907 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25908 })
25909 })
25910 .unwrap();
25911 close.await.unwrap();
25912 let close = workspace
25913 .update(cx, |workspace, window, cx| {
25914 workspace.active_pane().update(cx, |pane, cx| {
25915 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25916 })
25917 })
25918 .unwrap();
25919 close.await.unwrap();
25920 assert_eq!(
25921 2,
25922 requests_made.load(atomic::Ordering::Acquire),
25923 "After saving and closing all editors, no extra requests should be made"
25924 );
25925 workspace
25926 .update(cx, |workspace, _, cx| {
25927 assert!(
25928 workspace.active_item(cx).is_none(),
25929 "Should close all editors"
25930 )
25931 })
25932 .unwrap();
25933
25934 workspace
25935 .update(cx, |workspace, window, cx| {
25936 workspace.active_pane().update(cx, |pane, cx| {
25937 pane.navigate_backward(&workspace::GoBack, window, cx);
25938 })
25939 })
25940 .unwrap();
25941 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
25942 cx.run_until_parked();
25943 let editor = workspace
25944 .update(cx, |workspace, _, cx| {
25945 workspace
25946 .active_item(cx)
25947 .expect("Should have reopened the editor again after navigating back")
25948 .downcast::<Editor>()
25949 .expect("Should be an editor")
25950 })
25951 .unwrap();
25952
25953 assert_eq!(
25954 2,
25955 requests_made.load(atomic::Ordering::Acquire),
25956 "Cache should be reused on buffer close and reopen"
25957 );
25958 editor.update(cx, |editor, cx| {
25959 assert_eq!(
25960 vec![expected_color],
25961 extract_color_inlays(editor, cx),
25962 "Should have an initial inlay"
25963 );
25964 });
25965
25966 drop(color_request_handle);
25967 let closure_requests_made = Arc::clone(&requests_made);
25968 let mut empty_color_request_handle = fake_language_server
25969 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25970 let requests_made = Arc::clone(&closure_requests_made);
25971 async move {
25972 assert_eq!(
25973 params.text_document.uri,
25974 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25975 );
25976 requests_made.fetch_add(1, atomic::Ordering::Release);
25977 Ok(Vec::new())
25978 }
25979 });
25980 let save = editor.update_in(cx, |editor, window, cx| {
25981 editor.move_to_end(&MoveToEnd, window, cx);
25982 editor.handle_input("dirty_again", window, cx);
25983 editor.save(
25984 SaveOptions {
25985 format: false,
25986 autosave: true,
25987 },
25988 project.clone(),
25989 window,
25990 cx,
25991 )
25992 });
25993 save.await.unwrap();
25994
25995 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
25996 empty_color_request_handle.next().await.unwrap();
25997 cx.run_until_parked();
25998 assert_eq!(
25999 3,
26000 requests_made.load(atomic::Ordering::Acquire),
26001 "Should query for colors once per save only, as formatting was not requested"
26002 );
26003 editor.update(cx, |editor, cx| {
26004 assert_eq!(
26005 Vec::<Rgba>::new(),
26006 extract_color_inlays(editor, cx),
26007 "Should clear all colors when the server returns an empty response"
26008 );
26009 });
26010}
26011
26012#[gpui::test]
26013async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26014 init_test(cx, |_| {});
26015 let (editor, cx) = cx.add_window_view(Editor::single_line);
26016 editor.update_in(cx, |editor, window, cx| {
26017 editor.set_text("oops\n\nwow\n", window, cx)
26018 });
26019 cx.run_until_parked();
26020 editor.update(cx, |editor, cx| {
26021 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26022 });
26023 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
26024 cx.run_until_parked();
26025 editor.update(cx, |editor, cx| {
26026 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26027 });
26028}
26029
26030#[gpui::test]
26031async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26032 init_test(cx, |_| {});
26033
26034 cx.update(|cx| {
26035 register_project_item::<Editor>(cx);
26036 });
26037
26038 let fs = FakeFs::new(cx.executor());
26039 fs.insert_tree("/root1", json!({})).await;
26040 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26041 .await;
26042
26043 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26044 let (workspace, cx) =
26045 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26046
26047 let worktree_id = project.update(cx, |project, cx| {
26048 project.worktrees(cx).next().unwrap().read(cx).id()
26049 });
26050
26051 let handle = workspace
26052 .update_in(cx, |workspace, window, cx| {
26053 let project_path = (worktree_id, rel_path("one.pdf"));
26054 workspace.open_path(project_path, None, true, window, cx)
26055 })
26056 .await
26057 .unwrap();
26058
26059 assert_eq!(
26060 handle.to_any().entity_type(),
26061 TypeId::of::<InvalidBufferView>()
26062 );
26063}
26064
26065#[gpui::test]
26066async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26067 init_test(cx, |_| {});
26068
26069 let language = Arc::new(Language::new(
26070 LanguageConfig::default(),
26071 Some(tree_sitter_rust::LANGUAGE.into()),
26072 ));
26073
26074 // Test hierarchical sibling navigation
26075 let text = r#"
26076 fn outer() {
26077 if condition {
26078 let a = 1;
26079 }
26080 let b = 2;
26081 }
26082
26083 fn another() {
26084 let c = 3;
26085 }
26086 "#;
26087
26088 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
26089 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
26090 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
26091
26092 // Wait for parsing to complete
26093 editor
26094 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
26095 .await;
26096
26097 editor.update_in(cx, |editor, window, cx| {
26098 // Start by selecting "let a = 1;" inside the if block
26099 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26100 s.select_display_ranges([
26101 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
26102 ]);
26103 });
26104
26105 let initial_selection = editor.selections.display_ranges(cx);
26106 assert_eq!(initial_selection.len(), 1, "Should have one selection");
26107
26108 // Test select next sibling - should move up levels to find the next sibling
26109 // Since "let a = 1;" has no siblings in the if block, it should move up
26110 // to find "let b = 2;" which is a sibling of the if block
26111 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26112 let next_selection = editor.selections.display_ranges(cx);
26113
26114 // Should have a selection and it should be different from the initial
26115 assert_eq!(
26116 next_selection.len(),
26117 1,
26118 "Should have one selection after next"
26119 );
26120 assert_ne!(
26121 next_selection[0], initial_selection[0],
26122 "Next sibling selection should be different"
26123 );
26124
26125 // Test hierarchical navigation by going to the end of the current function
26126 // and trying to navigate to the next function
26127 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26128 s.select_display_ranges([
26129 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26130 ]);
26131 });
26132
26133 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26134 let function_next_selection = editor.selections.display_ranges(cx);
26135
26136 // Should move to the next function
26137 assert_eq!(
26138 function_next_selection.len(),
26139 1,
26140 "Should have one selection after function next"
26141 );
26142
26143 // Test select previous sibling navigation
26144 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26145 let prev_selection = editor.selections.display_ranges(cx);
26146
26147 // Should have a selection and it should be different
26148 assert_eq!(
26149 prev_selection.len(),
26150 1,
26151 "Should have one selection after prev"
26152 );
26153 assert_ne!(
26154 prev_selection[0], function_next_selection[0],
26155 "Previous sibling selection should be different from next"
26156 );
26157 });
26158}
26159
26160#[gpui::test]
26161async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26162 init_test(cx, |_| {});
26163
26164 let mut cx = EditorTestContext::new(cx).await;
26165 cx.set_state(
26166 "let ˇvariable = 42;
26167let another = variable + 1;
26168let result = variable * 2;",
26169 );
26170
26171 // Set up document highlights manually (simulating LSP response)
26172 cx.update_editor(|editor, _window, cx| {
26173 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26174
26175 // Create highlights for "variable" occurrences
26176 let highlight_ranges = [
26177 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26178 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26179 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26180 ];
26181
26182 let anchor_ranges: Vec<_> = highlight_ranges
26183 .iter()
26184 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26185 .collect();
26186
26187 editor.highlight_background::<DocumentHighlightRead>(
26188 &anchor_ranges,
26189 |theme| theme.colors().editor_document_highlight_read_background,
26190 cx,
26191 );
26192 });
26193
26194 // Go to next highlight - should move to second "variable"
26195 cx.update_editor(|editor, window, cx| {
26196 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26197 });
26198 cx.assert_editor_state(
26199 "let variable = 42;
26200let another = ˇvariable + 1;
26201let result = variable * 2;",
26202 );
26203
26204 // Go to next highlight - should move to third "variable"
26205 cx.update_editor(|editor, window, cx| {
26206 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26207 });
26208 cx.assert_editor_state(
26209 "let variable = 42;
26210let another = variable + 1;
26211let result = ˇvariable * 2;",
26212 );
26213
26214 // Go to next highlight - should stay at third "variable" (no wrap-around)
26215 cx.update_editor(|editor, window, cx| {
26216 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26217 });
26218 cx.assert_editor_state(
26219 "let variable = 42;
26220let another = variable + 1;
26221let result = ˇvariable * 2;",
26222 );
26223
26224 // Now test going backwards from third position
26225 cx.update_editor(|editor, window, cx| {
26226 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26227 });
26228 cx.assert_editor_state(
26229 "let variable = 42;
26230let another = ˇvariable + 1;
26231let result = variable * 2;",
26232 );
26233
26234 // Go to previous highlight - should move to first "variable"
26235 cx.update_editor(|editor, window, cx| {
26236 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26237 });
26238 cx.assert_editor_state(
26239 "let ˇvariable = 42;
26240let another = variable + 1;
26241let result = variable * 2;",
26242 );
26243
26244 // Go to previous highlight - should stay on first "variable"
26245 cx.update_editor(|editor, window, cx| {
26246 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26247 });
26248 cx.assert_editor_state(
26249 "let ˇvariable = 42;
26250let another = variable + 1;
26251let result = variable * 2;",
26252 );
26253}
26254
26255#[gpui::test]
26256async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26257 cx: &mut gpui::TestAppContext,
26258) {
26259 init_test(cx, |_| {});
26260
26261 let url = "https://zed.dev";
26262
26263 let markdown_language = Arc::new(Language::new(
26264 LanguageConfig {
26265 name: "Markdown".into(),
26266 ..LanguageConfig::default()
26267 },
26268 None,
26269 ));
26270
26271 let mut cx = EditorTestContext::new(cx).await;
26272 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26273 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26274
26275 cx.update_editor(|editor, window, cx| {
26276 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26277 editor.paste(&Paste, window, cx);
26278 });
26279
26280 cx.assert_editor_state(&format!(
26281 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26282 ));
26283}
26284
26285#[gpui::test]
26286async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26287 cx: &mut gpui::TestAppContext,
26288) {
26289 init_test(cx, |_| {});
26290
26291 let url = "https://zed.dev";
26292
26293 let markdown_language = Arc::new(Language::new(
26294 LanguageConfig {
26295 name: "Markdown".into(),
26296 ..LanguageConfig::default()
26297 },
26298 None,
26299 ));
26300
26301 let mut cx = EditorTestContext::new(cx).await;
26302 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26303 cx.set_state(&format!(
26304 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26305 ));
26306
26307 cx.update_editor(|editor, window, cx| {
26308 editor.copy(&Copy, window, cx);
26309 });
26310
26311 cx.set_state(&format!(
26312 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26313 ));
26314
26315 cx.update_editor(|editor, window, cx| {
26316 editor.paste(&Paste, window, cx);
26317 });
26318
26319 cx.assert_editor_state(&format!(
26320 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26321 ));
26322}
26323
26324#[gpui::test]
26325async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26326 cx: &mut gpui::TestAppContext,
26327) {
26328 init_test(cx, |_| {});
26329
26330 let url = "https://zed.dev";
26331
26332 let markdown_language = Arc::new(Language::new(
26333 LanguageConfig {
26334 name: "Markdown".into(),
26335 ..LanguageConfig::default()
26336 },
26337 None,
26338 ));
26339
26340 let mut cx = EditorTestContext::new(cx).await;
26341 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26342 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26343
26344 cx.update_editor(|editor, window, cx| {
26345 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26346 editor.paste(&Paste, window, cx);
26347 });
26348
26349 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26350}
26351
26352#[gpui::test]
26353async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26354 cx: &mut gpui::TestAppContext,
26355) {
26356 init_test(cx, |_| {});
26357
26358 let text = "Awesome";
26359
26360 let markdown_language = Arc::new(Language::new(
26361 LanguageConfig {
26362 name: "Markdown".into(),
26363 ..LanguageConfig::default()
26364 },
26365 None,
26366 ));
26367
26368 let mut cx = EditorTestContext::new(cx).await;
26369 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26370 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26371
26372 cx.update_editor(|editor, window, cx| {
26373 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26374 editor.paste(&Paste, window, cx);
26375 });
26376
26377 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26378}
26379
26380#[gpui::test]
26381async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26382 cx: &mut gpui::TestAppContext,
26383) {
26384 init_test(cx, |_| {});
26385
26386 let url = "https://zed.dev";
26387
26388 let markdown_language = Arc::new(Language::new(
26389 LanguageConfig {
26390 name: "Rust".into(),
26391 ..LanguageConfig::default()
26392 },
26393 None,
26394 ));
26395
26396 let mut cx = EditorTestContext::new(cx).await;
26397 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26398 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26399
26400 cx.update_editor(|editor, window, cx| {
26401 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26402 editor.paste(&Paste, window, cx);
26403 });
26404
26405 cx.assert_editor_state(&format!(
26406 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26407 ));
26408}
26409
26410#[gpui::test]
26411async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26412 cx: &mut TestAppContext,
26413) {
26414 init_test(cx, |_| {});
26415
26416 let url = "https://zed.dev";
26417
26418 let markdown_language = Arc::new(Language::new(
26419 LanguageConfig {
26420 name: "Markdown".into(),
26421 ..LanguageConfig::default()
26422 },
26423 None,
26424 ));
26425
26426 let (editor, cx) = cx.add_window_view(|window, cx| {
26427 let multi_buffer = MultiBuffer::build_multi(
26428 [
26429 ("this will embed -> link", vec![Point::row_range(0..1)]),
26430 ("this will replace -> link", vec![Point::row_range(0..1)]),
26431 ],
26432 cx,
26433 );
26434 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26435 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26436 s.select_ranges(vec![
26437 Point::new(0, 19)..Point::new(0, 23),
26438 Point::new(1, 21)..Point::new(1, 25),
26439 ])
26440 });
26441 let first_buffer_id = multi_buffer
26442 .read(cx)
26443 .excerpt_buffer_ids()
26444 .into_iter()
26445 .next()
26446 .unwrap();
26447 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26448 first_buffer.update(cx, |buffer, cx| {
26449 buffer.set_language(Some(markdown_language.clone()), cx);
26450 });
26451
26452 editor
26453 });
26454 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26455
26456 cx.update_editor(|editor, window, cx| {
26457 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26458 editor.paste(&Paste, window, cx);
26459 });
26460
26461 cx.assert_editor_state(&format!(
26462 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26463 ));
26464}
26465
26466#[gpui::test]
26467async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26468 init_test(cx, |_| {});
26469
26470 let fs = FakeFs::new(cx.executor());
26471 fs.insert_tree(
26472 path!("/project"),
26473 json!({
26474 "first.rs": "# First Document\nSome content here.",
26475 "second.rs": "Plain text content for second file.",
26476 }),
26477 )
26478 .await;
26479
26480 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26481 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26482 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26483
26484 let language = rust_lang();
26485 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26486 language_registry.add(language.clone());
26487 let mut fake_servers = language_registry.register_fake_lsp(
26488 "Rust",
26489 FakeLspAdapter {
26490 ..FakeLspAdapter::default()
26491 },
26492 );
26493
26494 let buffer1 = project
26495 .update(cx, |project, cx| {
26496 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26497 })
26498 .await
26499 .unwrap();
26500 let buffer2 = project
26501 .update(cx, |project, cx| {
26502 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26503 })
26504 .await
26505 .unwrap();
26506
26507 let multi_buffer = cx.new(|cx| {
26508 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26509 multi_buffer.set_excerpts_for_path(
26510 PathKey::for_buffer(&buffer1, cx),
26511 buffer1.clone(),
26512 [Point::zero()..buffer1.read(cx).max_point()],
26513 3,
26514 cx,
26515 );
26516 multi_buffer.set_excerpts_for_path(
26517 PathKey::for_buffer(&buffer2, cx),
26518 buffer2.clone(),
26519 [Point::zero()..buffer1.read(cx).max_point()],
26520 3,
26521 cx,
26522 );
26523 multi_buffer
26524 });
26525
26526 let (editor, cx) = cx.add_window_view(|window, cx| {
26527 Editor::new(
26528 EditorMode::full(),
26529 multi_buffer,
26530 Some(project.clone()),
26531 window,
26532 cx,
26533 )
26534 });
26535
26536 let fake_language_server = fake_servers.next().await.unwrap();
26537
26538 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26539
26540 let save = editor.update_in(cx, |editor, window, cx| {
26541 assert!(editor.is_dirty(cx));
26542
26543 editor.save(
26544 SaveOptions {
26545 format: true,
26546 autosave: true,
26547 },
26548 project,
26549 window,
26550 cx,
26551 )
26552 });
26553 let (start_edit_tx, start_edit_rx) = oneshot::channel();
26554 let (done_edit_tx, done_edit_rx) = oneshot::channel();
26555 let mut done_edit_rx = Some(done_edit_rx);
26556 let mut start_edit_tx = Some(start_edit_tx);
26557
26558 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
26559 start_edit_tx.take().unwrap().send(()).unwrap();
26560 let done_edit_rx = done_edit_rx.take().unwrap();
26561 async move {
26562 done_edit_rx.await.unwrap();
26563 Ok(None)
26564 }
26565 });
26566
26567 start_edit_rx.await.unwrap();
26568 buffer2
26569 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
26570 .unwrap();
26571
26572 done_edit_tx.send(()).unwrap();
26573
26574 save.await.unwrap();
26575 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
26576}
26577
26578#[track_caller]
26579fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26580 editor
26581 .all_inlays(cx)
26582 .into_iter()
26583 .filter_map(|inlay| inlay.get_color())
26584 .map(Rgba::from)
26585 .collect()
26586}
26587
26588#[gpui::test]
26589fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
26590 init_test(cx, |_| {});
26591
26592 let editor = cx.add_window(|window, cx| {
26593 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
26594 build_editor(buffer, window, cx)
26595 });
26596
26597 editor
26598 .update(cx, |editor, window, cx| {
26599 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26600 s.select_display_ranges([
26601 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
26602 ])
26603 });
26604
26605 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
26606
26607 assert_eq!(
26608 editor.display_text(cx),
26609 "line1\nline2\nline2",
26610 "Duplicating last line upward should create duplicate above, not on same line"
26611 );
26612
26613 assert_eq!(
26614 editor.selections.display_ranges(cx),
26615 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)],
26616 "Selection should remain on the original line"
26617 );
26618 })
26619 .unwrap();
26620}
26621
26622#[gpui::test]
26623async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
26624 init_test(cx, |_| {});
26625
26626 let mut cx = EditorTestContext::new(cx).await;
26627
26628 cx.set_state("line1\nline2ˇ");
26629
26630 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
26631
26632 let clipboard_text = cx
26633 .read_from_clipboard()
26634 .and_then(|item| item.text().as_deref().map(str::to_string));
26635
26636 assert_eq!(
26637 clipboard_text,
26638 Some("line2\n".to_string()),
26639 "Copying a line without trailing newline should include a newline"
26640 );
26641
26642 cx.set_state("line1\nˇ");
26643
26644 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
26645
26646 cx.assert_editor_state("line1\nline2\nˇ");
26647}