1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionDelegate,
6 element::StickyHeader,
7 linked_editing_ranges::LinkedEditingRanges,
8 scroll::scroll_amount::ScrollAmount,
9 test::{
10 assert_text_with_selections, build_editor,
11 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
12 editor_test_context::EditorTestContext,
13 select_ranges,
14 },
15};
16use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
17use collections::HashMap;
18use futures::{StreamExt, channel::oneshot};
19use gpui::{
20 BackgroundExecutor, DismissEvent, Rgba, TestAppContext, UpdateGlobal, VisualTestContext,
21 WindowBounds, WindowOptions, div,
22};
23use indoc::indoc;
24use language::{
25 BracketPairConfig,
26 Capability::ReadWrite,
27 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
28 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
29 language_settings::{
30 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
31 },
32 tree_sitter_python,
33};
34use language_settings::Formatter;
35use languages::markdown_lang;
36use languages::rust_lang;
37use lsp::CompletionParams;
38use multi_buffer::{
39 ExcerptRange, IndentGuide, MultiBuffer, MultiBufferFilterMode, MultiBufferOffset,
40 MultiBufferOffsetUtf16, PathKey,
41};
42use parking_lot::Mutex;
43use pretty_assertions::{assert_eq, assert_ne};
44use project::{
45 FakeFs, Project,
46 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
47 project_settings::LspSettings,
48 trusted_worktrees::{PathTrust, TrustedWorktrees},
49};
50use serde_json::{self, json};
51use settings::{
52 AllLanguageSettingsContent, EditorSettingsContent, IndentGuideBackgroundColoring,
53 IndentGuideColoring, InlayHintSettingsContent, ProjectSettingsContent, SearchSettingsContent,
54 SettingsStore,
55};
56use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
57use std::{
58 iter,
59 sync::atomic::{self, AtomicUsize},
60};
61use test::build_editor_with_project;
62use text::ToPoint as _;
63use unindent::Unindent;
64use util::{
65 assert_set_eq, path,
66 rel_path::rel_path,
67 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
68 uri,
69};
70use workspace::{
71 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
72 OpenOptions, ViewId,
73 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
74 register_project_item,
75};
76
77fn display_ranges(editor: &Editor, cx: &mut Context<'_, Editor>) -> Vec<Range<DisplayPoint>> {
78 editor
79 .selections
80 .display_ranges(&editor.display_snapshot(cx))
81}
82
83#[gpui::test]
84fn test_edit_events(cx: &mut TestAppContext) {
85 init_test(cx, |_| {});
86
87 let buffer = cx.new(|cx| {
88 let mut buffer = language::Buffer::local("123456", cx);
89 buffer.set_group_interval(Duration::from_secs(1));
90 buffer
91 });
92
93 let events = Rc::new(RefCell::new(Vec::new()));
94 let editor1 = cx.add_window({
95 let events = events.clone();
96 |window, cx| {
97 let entity = cx.entity();
98 cx.subscribe_in(
99 &entity,
100 window,
101 move |_, _, event: &EditorEvent, _, _| match event {
102 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
103 EditorEvent::BufferEdited => {
104 events.borrow_mut().push(("editor1", "buffer edited"))
105 }
106 _ => {}
107 },
108 )
109 .detach();
110 Editor::for_buffer(buffer.clone(), None, window, cx)
111 }
112 });
113
114 let editor2 = cx.add_window({
115 let events = events.clone();
116 |window, cx| {
117 cx.subscribe_in(
118 &cx.entity(),
119 window,
120 move |_, _, event: &EditorEvent, _, _| match event {
121 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
122 EditorEvent::BufferEdited => {
123 events.borrow_mut().push(("editor2", "buffer edited"))
124 }
125 _ => {}
126 },
127 )
128 .detach();
129 Editor::for_buffer(buffer.clone(), None, window, cx)
130 }
131 });
132
133 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
134
135 // Mutating editor 1 will emit an `Edited` event only for that editor.
136 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
137 assert_eq!(
138 mem::take(&mut *events.borrow_mut()),
139 [
140 ("editor1", "edited"),
141 ("editor1", "buffer edited"),
142 ("editor2", "buffer edited"),
143 ]
144 );
145
146 // Mutating editor 2 will emit an `Edited` event only for that editor.
147 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
148 assert_eq!(
149 mem::take(&mut *events.borrow_mut()),
150 [
151 ("editor2", "edited"),
152 ("editor1", "buffer edited"),
153 ("editor2", "buffer edited"),
154 ]
155 );
156
157 // Undoing on editor 1 will emit an `Edited` event only for that editor.
158 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
159 assert_eq!(
160 mem::take(&mut *events.borrow_mut()),
161 [
162 ("editor1", "edited"),
163 ("editor1", "buffer edited"),
164 ("editor2", "buffer edited"),
165 ]
166 );
167
168 // Redoing on editor 1 will emit an `Edited` event only for that editor.
169 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
170 assert_eq!(
171 mem::take(&mut *events.borrow_mut()),
172 [
173 ("editor1", "edited"),
174 ("editor1", "buffer edited"),
175 ("editor2", "buffer edited"),
176 ]
177 );
178
179 // Undoing on editor 2 will emit an `Edited` event only for that editor.
180 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
181 assert_eq!(
182 mem::take(&mut *events.borrow_mut()),
183 [
184 ("editor2", "edited"),
185 ("editor1", "buffer edited"),
186 ("editor2", "buffer edited"),
187 ]
188 );
189
190 // Redoing on editor 2 will emit an `Edited` event only for that editor.
191 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
192 assert_eq!(
193 mem::take(&mut *events.borrow_mut()),
194 [
195 ("editor2", "edited"),
196 ("editor1", "buffer edited"),
197 ("editor2", "buffer edited"),
198 ]
199 );
200
201 // No event is emitted when the mutation is a no-op.
202 _ = editor2.update(cx, |editor, window, cx| {
203 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
204 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
205 });
206
207 editor.backspace(&Backspace, window, cx);
208 });
209 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
210}
211
212#[gpui::test]
213fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
214 init_test(cx, |_| {});
215
216 let mut now = Instant::now();
217 let group_interval = Duration::from_millis(1);
218 let buffer = cx.new(|cx| {
219 let mut buf = language::Buffer::local("123456", cx);
220 buf.set_group_interval(group_interval);
221 buf
222 });
223 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
224 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
225
226 _ = editor.update(cx, |editor, window, cx| {
227 editor.start_transaction_at(now, window, cx);
228 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
229 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(4)])
230 });
231
232 editor.insert("cd", window, cx);
233 editor.end_transaction_at(now, cx);
234 assert_eq!(editor.text(cx), "12cd56");
235 assert_eq!(
236 editor.selections.ranges(&editor.display_snapshot(cx)),
237 vec![MultiBufferOffset(4)..MultiBufferOffset(4)]
238 );
239
240 editor.start_transaction_at(now, window, cx);
241 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
242 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(5)])
243 });
244 editor.insert("e", window, cx);
245 editor.end_transaction_at(now, cx);
246 assert_eq!(editor.text(cx), "12cde6");
247 assert_eq!(
248 editor.selections.ranges(&editor.display_snapshot(cx)),
249 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
250 );
251
252 now += group_interval + Duration::from_millis(1);
253 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
254 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(2)])
255 });
256
257 // Simulate an edit in another editor
258 buffer.update(cx, |buffer, cx| {
259 buffer.start_transaction_at(now, cx);
260 buffer.edit(
261 [(MultiBufferOffset(0)..MultiBufferOffset(1), "a")],
262 None,
263 cx,
264 );
265 buffer.edit(
266 [(MultiBufferOffset(1)..MultiBufferOffset(1), "b")],
267 None,
268 cx,
269 );
270 buffer.end_transaction_at(now, cx);
271 });
272
273 assert_eq!(editor.text(cx), "ab2cde6");
274 assert_eq!(
275 editor.selections.ranges(&editor.display_snapshot(cx)),
276 vec![MultiBufferOffset(3)..MultiBufferOffset(3)]
277 );
278
279 // Last transaction happened past the group interval in a different editor.
280 // Undo it individually and don't restore selections.
281 editor.undo(&Undo, window, cx);
282 assert_eq!(editor.text(cx), "12cde6");
283 assert_eq!(
284 editor.selections.ranges(&editor.display_snapshot(cx)),
285 vec![MultiBufferOffset(2)..MultiBufferOffset(2)]
286 );
287
288 // First two transactions happened within the group interval in this editor.
289 // Undo them together and restore selections.
290 editor.undo(&Undo, window, cx);
291 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
292 assert_eq!(editor.text(cx), "123456");
293 assert_eq!(
294 editor.selections.ranges(&editor.display_snapshot(cx)),
295 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
296 );
297
298 // Redo the first two transactions together.
299 editor.redo(&Redo, window, cx);
300 assert_eq!(editor.text(cx), "12cde6");
301 assert_eq!(
302 editor.selections.ranges(&editor.display_snapshot(cx)),
303 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
304 );
305
306 // Redo the last transaction on its own.
307 editor.redo(&Redo, window, cx);
308 assert_eq!(editor.text(cx), "ab2cde6");
309 assert_eq!(
310 editor.selections.ranges(&editor.display_snapshot(cx)),
311 vec![MultiBufferOffset(6)..MultiBufferOffset(6)]
312 );
313
314 // Test empty transactions.
315 editor.start_transaction_at(now, window, cx);
316 editor.end_transaction_at(now, cx);
317 editor.undo(&Undo, window, cx);
318 assert_eq!(editor.text(cx), "12cde6");
319 });
320}
321
322#[gpui::test]
323fn test_ime_composition(cx: &mut TestAppContext) {
324 init_test(cx, |_| {});
325
326 let buffer = cx.new(|cx| {
327 let mut buffer = language::Buffer::local("abcde", cx);
328 // Ensure automatic grouping doesn't occur.
329 buffer.set_group_interval(Duration::ZERO);
330 buffer
331 });
332
333 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
334 cx.add_window(|window, cx| {
335 let mut editor = build_editor(buffer.clone(), window, cx);
336
337 // Start a new IME composition.
338 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
339 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
340 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
341 assert_eq!(editor.text(cx), "äbcde");
342 assert_eq!(
343 editor.marked_text_ranges(cx),
344 Some(vec![
345 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
346 ])
347 );
348
349 // Finalize IME composition.
350 editor.replace_text_in_range(None, "ā", window, cx);
351 assert_eq!(editor.text(cx), "ābcde");
352 assert_eq!(editor.marked_text_ranges(cx), None);
353
354 // IME composition edits are grouped and are undone/redone at once.
355 editor.undo(&Default::default(), window, cx);
356 assert_eq!(editor.text(cx), "abcde");
357 assert_eq!(editor.marked_text_ranges(cx), None);
358 editor.redo(&Default::default(), window, cx);
359 assert_eq!(editor.text(cx), "ābcde");
360 assert_eq!(editor.marked_text_ranges(cx), None);
361
362 // Start a new IME composition.
363 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
364 assert_eq!(
365 editor.marked_text_ranges(cx),
366 Some(vec![
367 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
368 ])
369 );
370
371 // Undoing during an IME composition cancels it.
372 editor.undo(&Default::default(), window, cx);
373 assert_eq!(editor.text(cx), "ābcde");
374 assert_eq!(editor.marked_text_ranges(cx), None);
375
376 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
377 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
378 assert_eq!(editor.text(cx), "ābcdè");
379 assert_eq!(
380 editor.marked_text_ranges(cx),
381 Some(vec![
382 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(5))
383 ])
384 );
385
386 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
387 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
388 assert_eq!(editor.text(cx), "ābcdę");
389 assert_eq!(editor.marked_text_ranges(cx), None);
390
391 // Start a new IME composition with multiple cursors.
392 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
393 s.select_ranges([
394 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(1)),
395 MultiBufferOffsetUtf16(OffsetUtf16(3))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
396 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(5)),
397 ])
398 });
399 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
400 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
401 assert_eq!(
402 editor.marked_text_ranges(cx),
403 Some(vec![
404 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
405 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(7)),
406 MultiBufferOffsetUtf16(OffsetUtf16(8))..MultiBufferOffsetUtf16(OffsetUtf16(11))
407 ])
408 );
409
410 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
411 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
412 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
413 assert_eq!(
414 editor.marked_text_ranges(cx),
415 Some(vec![
416 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(2)),
417 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(6)),
418 MultiBufferOffsetUtf16(OffsetUtf16(9))..MultiBufferOffsetUtf16(OffsetUtf16(10))
419 ])
420 );
421
422 // Finalize IME composition with multiple cursors.
423 editor.replace_text_in_range(Some(9..10), "2", window, cx);
424 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
425 assert_eq!(editor.marked_text_ranges(cx), None);
426
427 editor
428 });
429}
430
431#[gpui::test]
432fn test_selection_with_mouse(cx: &mut TestAppContext) {
433 init_test(cx, |_| {});
434
435 let editor = cx.add_window(|window, cx| {
436 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
437 build_editor(buffer, window, cx)
438 });
439
440 _ = editor.update(cx, |editor, window, cx| {
441 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
442 });
443 assert_eq!(
444 editor
445 .update(cx, |editor, _, cx| display_ranges(editor, cx))
446 .unwrap(),
447 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
448 );
449
450 _ = editor.update(cx, |editor, window, cx| {
451 editor.update_selection(
452 DisplayPoint::new(DisplayRow(3), 3),
453 0,
454 gpui::Point::<f32>::default(),
455 window,
456 cx,
457 );
458 });
459
460 assert_eq!(
461 editor
462 .update(cx, |editor, _, cx| display_ranges(editor, cx))
463 .unwrap(),
464 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
465 );
466
467 _ = editor.update(cx, |editor, window, cx| {
468 editor.update_selection(
469 DisplayPoint::new(DisplayRow(1), 1),
470 0,
471 gpui::Point::<f32>::default(),
472 window,
473 cx,
474 );
475 });
476
477 assert_eq!(
478 editor
479 .update(cx, |editor, _, cx| display_ranges(editor, cx))
480 .unwrap(),
481 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
482 );
483
484 _ = editor.update(cx, |editor, window, cx| {
485 editor.end_selection(window, cx);
486 editor.update_selection(
487 DisplayPoint::new(DisplayRow(3), 3),
488 0,
489 gpui::Point::<f32>::default(),
490 window,
491 cx,
492 );
493 });
494
495 assert_eq!(
496 editor
497 .update(cx, |editor, _, cx| display_ranges(editor, cx))
498 .unwrap(),
499 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
500 );
501
502 _ = editor.update(cx, |editor, window, cx| {
503 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
504 editor.update_selection(
505 DisplayPoint::new(DisplayRow(0), 0),
506 0,
507 gpui::Point::<f32>::default(),
508 window,
509 cx,
510 );
511 });
512
513 assert_eq!(
514 editor
515 .update(cx, |editor, _, cx| display_ranges(editor, cx))
516 .unwrap(),
517 [
518 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
519 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
520 ]
521 );
522
523 _ = editor.update(cx, |editor, window, cx| {
524 editor.end_selection(window, cx);
525 });
526
527 assert_eq!(
528 editor
529 .update(cx, |editor, _, cx| display_ranges(editor, cx))
530 .unwrap(),
531 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
532 );
533}
534
535#[gpui::test]
536fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
537 init_test(cx, |_| {});
538
539 let editor = cx.add_window(|window, cx| {
540 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
541 build_editor(buffer, window, cx)
542 });
543
544 _ = editor.update(cx, |editor, window, cx| {
545 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.end_selection(window, cx);
550 });
551
552 _ = editor.update(cx, |editor, window, cx| {
553 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
554 });
555
556 _ = editor.update(cx, |editor, window, cx| {
557 editor.end_selection(window, cx);
558 });
559
560 assert_eq!(
561 editor
562 .update(cx, |editor, _, cx| display_ranges(editor, cx))
563 .unwrap(),
564 [
565 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
566 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
567 ]
568 );
569
570 _ = editor.update(cx, |editor, window, cx| {
571 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
572 });
573
574 _ = editor.update(cx, |editor, window, cx| {
575 editor.end_selection(window, cx);
576 });
577
578 assert_eq!(
579 editor
580 .update(cx, |editor, _, cx| display_ranges(editor, cx))
581 .unwrap(),
582 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
583 );
584}
585
586#[gpui::test]
587fn test_canceling_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 display_ranges(editor, cx),
599 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
600 );
601 });
602
603 _ = editor.update(cx, |editor, window, cx| {
604 editor.update_selection(
605 DisplayPoint::new(DisplayRow(3), 3),
606 0,
607 gpui::Point::<f32>::default(),
608 window,
609 cx,
610 );
611 assert_eq!(
612 display_ranges(editor, cx),
613 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
614 );
615 });
616
617 _ = editor.update(cx, |editor, window, cx| {
618 editor.cancel(&Cancel, window, cx);
619 editor.update_selection(
620 DisplayPoint::new(DisplayRow(1), 1),
621 0,
622 gpui::Point::<f32>::default(),
623 window,
624 cx,
625 );
626 assert_eq!(
627 display_ranges(editor, cx),
628 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
629 );
630 });
631}
632
633#[gpui::test]
634fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
635 init_test(cx, |_| {});
636
637 let editor = cx.add_window(|window, cx| {
638 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
639 build_editor(buffer, window, cx)
640 });
641
642 _ = editor.update(cx, |editor, window, cx| {
643 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
644 assert_eq!(
645 display_ranges(editor, cx),
646 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
647 );
648
649 editor.move_down(&Default::default(), window, cx);
650 assert_eq!(
651 display_ranges(editor, cx),
652 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
653 );
654
655 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
656 assert_eq!(
657 display_ranges(editor, cx),
658 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
659 );
660
661 editor.move_up(&Default::default(), window, cx);
662 assert_eq!(
663 display_ranges(editor, cx),
664 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
665 );
666 });
667}
668
669#[gpui::test]
670fn test_extending_selection(cx: &mut TestAppContext) {
671 init_test(cx, |_| {});
672
673 let editor = cx.add_window(|window, cx| {
674 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
675 build_editor(buffer, window, cx)
676 });
677
678 _ = editor.update(cx, |editor, window, cx| {
679 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
680 editor.end_selection(window, cx);
681 assert_eq!(
682 display_ranges(editor, cx),
683 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
684 );
685
686 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
687 editor.end_selection(window, cx);
688 assert_eq!(
689 display_ranges(editor, cx),
690 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
691 );
692
693 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
694 editor.end_selection(window, cx);
695 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
696 assert_eq!(
697 display_ranges(editor, cx),
698 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
699 );
700
701 editor.update_selection(
702 DisplayPoint::new(DisplayRow(0), 1),
703 0,
704 gpui::Point::<f32>::default(),
705 window,
706 cx,
707 );
708 editor.end_selection(window, cx);
709 assert_eq!(
710 display_ranges(editor, cx),
711 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
712 );
713
714 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
715 editor.end_selection(window, cx);
716 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
717 editor.end_selection(window, cx);
718 assert_eq!(
719 display_ranges(editor, cx),
720 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
721 );
722
723 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
724 assert_eq!(
725 display_ranges(editor, cx),
726 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
727 );
728
729 editor.update_selection(
730 DisplayPoint::new(DisplayRow(0), 6),
731 0,
732 gpui::Point::<f32>::default(),
733 window,
734 cx,
735 );
736 assert_eq!(
737 display_ranges(editor, cx),
738 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
739 );
740
741 editor.update_selection(
742 DisplayPoint::new(DisplayRow(0), 1),
743 0,
744 gpui::Point::<f32>::default(),
745 window,
746 cx,
747 );
748 editor.end_selection(window, cx);
749 assert_eq!(
750 display_ranges(editor, cx),
751 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
752 );
753 });
754}
755
756#[gpui::test]
757fn test_clone(cx: &mut TestAppContext) {
758 init_test(cx, |_| {});
759
760 let (text, selection_ranges) = marked_text_ranges(
761 indoc! {"
762 one
763 two
764 threeˇ
765 four
766 fiveˇ
767 "},
768 true,
769 );
770
771 let editor = cx.add_window(|window, cx| {
772 let buffer = MultiBuffer::build_simple(&text, cx);
773 build_editor(buffer, window, cx)
774 });
775
776 _ = editor.update(cx, |editor, window, cx| {
777 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
778 s.select_ranges(
779 selection_ranges
780 .iter()
781 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
782 )
783 });
784 editor.fold_creases(
785 vec![
786 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
787 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
788 ],
789 true,
790 window,
791 cx,
792 );
793 });
794
795 let cloned_editor = editor
796 .update(cx, |editor, _, cx| {
797 cx.open_window(Default::default(), |window, cx| {
798 cx.new(|cx| editor.clone(window, cx))
799 })
800 })
801 .unwrap()
802 .unwrap();
803
804 let snapshot = editor
805 .update(cx, |e, window, cx| e.snapshot(window, cx))
806 .unwrap();
807 let cloned_snapshot = cloned_editor
808 .update(cx, |e, window, cx| e.snapshot(window, cx))
809 .unwrap();
810
811 assert_eq!(
812 cloned_editor
813 .update(cx, |e, _, cx| e.display_text(cx))
814 .unwrap(),
815 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
816 );
817 assert_eq!(
818 cloned_snapshot
819 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
820 .collect::<Vec<_>>(),
821 snapshot
822 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
823 .collect::<Vec<_>>(),
824 );
825 assert_set_eq!(
826 cloned_editor
827 .update(cx, |editor, _, cx| editor
828 .selections
829 .ranges::<Point>(&editor.display_snapshot(cx)))
830 .unwrap(),
831 editor
832 .update(cx, |editor, _, cx| editor
833 .selections
834 .ranges(&editor.display_snapshot(cx)))
835 .unwrap()
836 );
837 assert_set_eq!(
838 cloned_editor
839 .update(cx, |e, _window, cx| e
840 .selections
841 .display_ranges(&e.display_snapshot(cx)))
842 .unwrap(),
843 editor
844 .update(cx, |e, _, cx| e
845 .selections
846 .display_ranges(&e.display_snapshot(cx)))
847 .unwrap()
848 );
849}
850
851#[gpui::test]
852async fn test_navigation_history(cx: &mut TestAppContext) {
853 init_test(cx, |_| {});
854
855 use workspace::item::Item;
856
857 let fs = FakeFs::new(cx.executor());
858 let project = Project::test(fs, [], cx).await;
859 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
860 let pane = workspace
861 .update(cx, |workspace, _, _| workspace.active_pane().clone())
862 .unwrap();
863
864 _ = workspace.update(cx, |_v, window, cx| {
865 cx.new(|cx| {
866 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
867 let mut editor = build_editor(buffer, window, cx);
868 let handle = cx.entity();
869 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
870
871 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
872 editor.nav_history.as_mut().unwrap().pop_backward(cx)
873 }
874
875 // Move the cursor a small distance.
876 // Nothing is added to the navigation history.
877 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
878 s.select_display_ranges([
879 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
880 ])
881 });
882 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
883 s.select_display_ranges([
884 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
885 ])
886 });
887 assert!(pop_history(&mut editor, cx).is_none());
888
889 // Move the cursor a large distance.
890 // The history can jump back to the previous position.
891 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
892 s.select_display_ranges([
893 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
894 ])
895 });
896 let nav_entry = pop_history(&mut editor, cx).unwrap();
897 editor.navigate(nav_entry.data.unwrap(), window, cx);
898 assert_eq!(nav_entry.item.id(), cx.entity_id());
899 assert_eq!(
900 editor
901 .selections
902 .display_ranges(&editor.display_snapshot(cx)),
903 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
904 );
905 assert!(pop_history(&mut editor, cx).is_none());
906
907 // Move the cursor a small distance via the mouse.
908 // Nothing is added to the navigation history.
909 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
910 editor.end_selection(window, cx);
911 assert_eq!(
912 editor
913 .selections
914 .display_ranges(&editor.display_snapshot(cx)),
915 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
916 );
917 assert!(pop_history(&mut editor, cx).is_none());
918
919 // Move the cursor a large distance via the mouse.
920 // The history can jump back to the previous position.
921 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
922 editor.end_selection(window, cx);
923 assert_eq!(
924 editor
925 .selections
926 .display_ranges(&editor.display_snapshot(cx)),
927 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
928 );
929 let nav_entry = pop_history(&mut editor, cx).unwrap();
930 editor.navigate(nav_entry.data.unwrap(), window, cx);
931 assert_eq!(nav_entry.item.id(), cx.entity_id());
932 assert_eq!(
933 editor
934 .selections
935 .display_ranges(&editor.display_snapshot(cx)),
936 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
937 );
938 assert!(pop_history(&mut editor, cx).is_none());
939
940 // Set scroll position to check later
941 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
942 let original_scroll_position = editor.scroll_manager.anchor();
943
944 // Jump to the end of the document and adjust scroll
945 editor.move_to_end(&MoveToEnd, window, cx);
946 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
947 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
948
949 let nav_entry = pop_history(&mut editor, cx).unwrap();
950 editor.navigate(nav_entry.data.unwrap(), window, cx);
951 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
952
953 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
954 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
955 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
956 let invalid_point = Point::new(9999, 0);
957 editor.navigate(
958 Box::new(NavigationData {
959 cursor_anchor: invalid_anchor,
960 cursor_position: invalid_point,
961 scroll_anchor: ScrollAnchor {
962 anchor: invalid_anchor,
963 offset: Default::default(),
964 },
965 scroll_top_row: invalid_point.row,
966 }),
967 window,
968 cx,
969 );
970 assert_eq!(
971 editor
972 .selections
973 .display_ranges(&editor.display_snapshot(cx)),
974 &[editor.max_point(cx)..editor.max_point(cx)]
975 );
976 assert_eq!(
977 editor.scroll_position(cx),
978 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
979 );
980
981 editor
982 })
983 });
984}
985
986#[gpui::test]
987fn test_cancel(cx: &mut TestAppContext) {
988 init_test(cx, |_| {});
989
990 let editor = cx.add_window(|window, cx| {
991 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
992 build_editor(buffer, window, cx)
993 });
994
995 _ = editor.update(cx, |editor, window, cx| {
996 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
997 editor.update_selection(
998 DisplayPoint::new(DisplayRow(1), 1),
999 0,
1000 gpui::Point::<f32>::default(),
1001 window,
1002 cx,
1003 );
1004 editor.end_selection(window, cx);
1005
1006 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
1007 editor.update_selection(
1008 DisplayPoint::new(DisplayRow(0), 3),
1009 0,
1010 gpui::Point::<f32>::default(),
1011 window,
1012 cx,
1013 );
1014 editor.end_selection(window, cx);
1015 assert_eq!(
1016 display_ranges(editor, cx),
1017 [
1018 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
1019 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
1020 ]
1021 );
1022 });
1023
1024 _ = editor.update(cx, |editor, window, cx| {
1025 editor.cancel(&Cancel, window, cx);
1026 assert_eq!(
1027 display_ranges(editor, cx),
1028 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
1029 );
1030 });
1031
1032 _ = editor.update(cx, |editor, window, cx| {
1033 editor.cancel(&Cancel, window, cx);
1034 assert_eq!(
1035 display_ranges(editor, cx),
1036 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
1037 );
1038 });
1039}
1040
1041#[gpui::test]
1042fn test_fold_action(cx: &mut TestAppContext) {
1043 init_test(cx, |_| {});
1044
1045 let editor = cx.add_window(|window, cx| {
1046 let buffer = MultiBuffer::build_simple(
1047 &"
1048 impl Foo {
1049 // Hello!
1050
1051 fn a() {
1052 1
1053 }
1054
1055 fn b() {
1056 2
1057 }
1058
1059 fn c() {
1060 3
1061 }
1062 }
1063 "
1064 .unindent(),
1065 cx,
1066 );
1067 build_editor(buffer, window, cx)
1068 });
1069
1070 _ = editor.update(cx, |editor, window, cx| {
1071 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1072 s.select_display_ranges([
1073 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1074 ]);
1075 });
1076 editor.fold(&Fold, window, cx);
1077 assert_eq!(
1078 editor.display_text(cx),
1079 "
1080 impl Foo {
1081 // Hello!
1082
1083 fn a() {
1084 1
1085 }
1086
1087 fn b() {⋯
1088 }
1089
1090 fn c() {⋯
1091 }
1092 }
1093 "
1094 .unindent(),
1095 );
1096
1097 editor.fold(&Fold, window, cx);
1098 assert_eq!(
1099 editor.display_text(cx),
1100 "
1101 impl Foo {⋯
1102 }
1103 "
1104 .unindent(),
1105 );
1106
1107 editor.unfold_lines(&UnfoldLines, window, cx);
1108 assert_eq!(
1109 editor.display_text(cx),
1110 "
1111 impl Foo {
1112 // Hello!
1113
1114 fn a() {
1115 1
1116 }
1117
1118 fn b() {⋯
1119 }
1120
1121 fn c() {⋯
1122 }
1123 }
1124 "
1125 .unindent(),
1126 );
1127
1128 editor.unfold_lines(&UnfoldLines, window, cx);
1129 assert_eq!(
1130 editor.display_text(cx),
1131 editor.buffer.read(cx).read(cx).text()
1132 );
1133 });
1134}
1135
1136#[gpui::test]
1137fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1138 init_test(cx, |_| {});
1139
1140 let editor = cx.add_window(|window, cx| {
1141 let buffer = MultiBuffer::build_simple(
1142 &"
1143 class Foo:
1144 # Hello!
1145
1146 def a():
1147 print(1)
1148
1149 def b():
1150 print(2)
1151
1152 def c():
1153 print(3)
1154 "
1155 .unindent(),
1156 cx,
1157 );
1158 build_editor(buffer, window, cx)
1159 });
1160
1161 _ = editor.update(cx, |editor, window, cx| {
1162 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1163 s.select_display_ranges([
1164 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1165 ]);
1166 });
1167 editor.fold(&Fold, window, cx);
1168 assert_eq!(
1169 editor.display_text(cx),
1170 "
1171 class Foo:
1172 # Hello!
1173
1174 def a():
1175 print(1)
1176
1177 def b():⋯
1178
1179 def c():⋯
1180 "
1181 .unindent(),
1182 );
1183
1184 editor.fold(&Fold, window, cx);
1185 assert_eq!(
1186 editor.display_text(cx),
1187 "
1188 class Foo:⋯
1189 "
1190 .unindent(),
1191 );
1192
1193 editor.unfold_lines(&UnfoldLines, window, cx);
1194 assert_eq!(
1195 editor.display_text(cx),
1196 "
1197 class Foo:
1198 # Hello!
1199
1200 def a():
1201 print(1)
1202
1203 def b():⋯
1204
1205 def c():⋯
1206 "
1207 .unindent(),
1208 );
1209
1210 editor.unfold_lines(&UnfoldLines, window, cx);
1211 assert_eq!(
1212 editor.display_text(cx),
1213 editor.buffer.read(cx).read(cx).text()
1214 );
1215 });
1216}
1217
1218#[gpui::test]
1219fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1220 init_test(cx, |_| {});
1221
1222 let editor = cx.add_window(|window, cx| {
1223 let buffer = MultiBuffer::build_simple(
1224 &"
1225 class Foo:
1226 # Hello!
1227
1228 def a():
1229 print(1)
1230
1231 def b():
1232 print(2)
1233
1234
1235 def c():
1236 print(3)
1237
1238
1239 "
1240 .unindent(),
1241 cx,
1242 );
1243 build_editor(buffer, window, cx)
1244 });
1245
1246 _ = editor.update(cx, |editor, window, cx| {
1247 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1248 s.select_display_ranges([
1249 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1250 ]);
1251 });
1252 editor.fold(&Fold, window, cx);
1253 assert_eq!(
1254 editor.display_text(cx),
1255 "
1256 class Foo:
1257 # Hello!
1258
1259 def a():
1260 print(1)
1261
1262 def b():⋯
1263
1264
1265 def c():⋯
1266
1267
1268 "
1269 .unindent(),
1270 );
1271
1272 editor.fold(&Fold, window, cx);
1273 assert_eq!(
1274 editor.display_text(cx),
1275 "
1276 class Foo:⋯
1277
1278
1279 "
1280 .unindent(),
1281 );
1282
1283 editor.unfold_lines(&UnfoldLines, window, cx);
1284 assert_eq!(
1285 editor.display_text(cx),
1286 "
1287 class Foo:
1288 # Hello!
1289
1290 def a():
1291 print(1)
1292
1293 def b():⋯
1294
1295
1296 def c():⋯
1297
1298
1299 "
1300 .unindent(),
1301 );
1302
1303 editor.unfold_lines(&UnfoldLines, window, cx);
1304 assert_eq!(
1305 editor.display_text(cx),
1306 editor.buffer.read(cx).read(cx).text()
1307 );
1308 });
1309}
1310
1311#[gpui::test]
1312fn test_fold_at_level(cx: &mut TestAppContext) {
1313 init_test(cx, |_| {});
1314
1315 let editor = cx.add_window(|window, cx| {
1316 let buffer = MultiBuffer::build_simple(
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 cx,
1341 );
1342 build_editor(buffer, window, cx)
1343 });
1344
1345 _ = editor.update(cx, |editor, window, cx| {
1346 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1347 assert_eq!(
1348 editor.display_text(cx),
1349 "
1350 class Foo:
1351 # Hello!
1352
1353 def a():⋯
1354
1355 def b():⋯
1356
1357
1358 class Bar:
1359 # World!
1360
1361 def a():⋯
1362
1363 def b():⋯
1364
1365
1366 "
1367 .unindent(),
1368 );
1369
1370 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1371 assert_eq!(
1372 editor.display_text(cx),
1373 "
1374 class Foo:⋯
1375
1376
1377 class Bar:⋯
1378
1379
1380 "
1381 .unindent(),
1382 );
1383
1384 editor.unfold_all(&UnfoldAll, window, cx);
1385 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1386 assert_eq!(
1387 editor.display_text(cx),
1388 "
1389 class Foo:
1390 # Hello!
1391
1392 def a():
1393 print(1)
1394
1395 def b():
1396 print(2)
1397
1398
1399 class Bar:
1400 # World!
1401
1402 def a():
1403 print(1)
1404
1405 def b():
1406 print(2)
1407
1408
1409 "
1410 .unindent(),
1411 );
1412
1413 assert_eq!(
1414 editor.display_text(cx),
1415 editor.buffer.read(cx).read(cx).text()
1416 );
1417 let (_, positions) = marked_text_ranges(
1418 &"
1419 class Foo:
1420 # Hello!
1421
1422 def a():
1423 print(1)
1424
1425 def b():
1426 p«riˇ»nt(2)
1427
1428
1429 class Bar:
1430 # World!
1431
1432 def a():
1433 «ˇprint(1)
1434
1435 def b():
1436 print(2)»
1437
1438
1439 "
1440 .unindent(),
1441 true,
1442 );
1443
1444 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1445 s.select_ranges(
1446 positions
1447 .iter()
1448 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
1449 )
1450 });
1451
1452 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1453 assert_eq!(
1454 editor.display_text(cx),
1455 "
1456 class Foo:
1457 # Hello!
1458
1459 def a():⋯
1460
1461 def b():
1462 print(2)
1463
1464
1465 class Bar:
1466 # World!
1467
1468 def a():
1469 print(1)
1470
1471 def b():
1472 print(2)
1473
1474
1475 "
1476 .unindent(),
1477 );
1478 });
1479}
1480
1481#[gpui::test]
1482fn test_move_cursor(cx: &mut TestAppContext) {
1483 init_test(cx, |_| {});
1484
1485 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1486 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1487
1488 buffer.update(cx, |buffer, cx| {
1489 buffer.edit(
1490 vec![
1491 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1492 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1493 ],
1494 None,
1495 cx,
1496 );
1497 });
1498 _ = editor.update(cx, |editor, window, cx| {
1499 assert_eq!(
1500 display_ranges(editor, cx),
1501 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1502 );
1503
1504 editor.move_down(&MoveDown, window, cx);
1505 assert_eq!(
1506 display_ranges(editor, cx),
1507 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1508 );
1509
1510 editor.move_right(&MoveRight, window, cx);
1511 assert_eq!(
1512 display_ranges(editor, cx),
1513 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1514 );
1515
1516 editor.move_left(&MoveLeft, window, cx);
1517 assert_eq!(
1518 display_ranges(editor, cx),
1519 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1520 );
1521
1522 editor.move_up(&MoveUp, window, cx);
1523 assert_eq!(
1524 display_ranges(editor, cx),
1525 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1526 );
1527
1528 editor.move_to_end(&MoveToEnd, window, cx);
1529 assert_eq!(
1530 display_ranges(editor, cx),
1531 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1532 );
1533
1534 editor.move_to_beginning(&MoveToBeginning, window, cx);
1535 assert_eq!(
1536 display_ranges(editor, cx),
1537 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1538 );
1539
1540 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1541 s.select_display_ranges([
1542 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1543 ]);
1544 });
1545 editor.select_to_beginning(&SelectToBeginning, window, cx);
1546 assert_eq!(
1547 display_ranges(editor, cx),
1548 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1549 );
1550
1551 editor.select_to_end(&SelectToEnd, window, cx);
1552 assert_eq!(
1553 display_ranges(editor, cx),
1554 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1555 );
1556 });
1557}
1558
1559#[gpui::test]
1560fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1561 init_test(cx, |_| {});
1562
1563 let editor = cx.add_window(|window, cx| {
1564 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1565 build_editor(buffer, window, cx)
1566 });
1567
1568 assert_eq!('🟥'.len_utf8(), 4);
1569 assert_eq!('α'.len_utf8(), 2);
1570
1571 _ = editor.update(cx, |editor, window, cx| {
1572 editor.fold_creases(
1573 vec![
1574 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1575 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1576 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1577 ],
1578 true,
1579 window,
1580 cx,
1581 );
1582 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1583
1584 editor.move_right(&MoveRight, window, cx);
1585 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1586 editor.move_right(&MoveRight, window, cx);
1587 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1588 editor.move_right(&MoveRight, window, cx);
1589 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧⋯".len())]);
1590
1591 editor.move_down(&MoveDown, window, cx);
1592 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1593 editor.move_left(&MoveLeft, window, cx);
1594 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯".len())]);
1595 editor.move_left(&MoveLeft, window, cx);
1596 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab".len())]);
1597 editor.move_left(&MoveLeft, window, cx);
1598 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "a".len())]);
1599
1600 editor.move_down(&MoveDown, window, cx);
1601 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "α".len())]);
1602 editor.move_right(&MoveRight, window, cx);
1603 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ".len())]);
1604 editor.move_right(&MoveRight, window, cx);
1605 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯".len())]);
1606 editor.move_right(&MoveRight, window, cx);
1607 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1608
1609 editor.move_up(&MoveUp, window, cx);
1610 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1611 editor.move_down(&MoveDown, window, cx);
1612 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1613 editor.move_up(&MoveUp, window, cx);
1614 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1615
1616 editor.move_up(&MoveUp, window, cx);
1617 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1618 editor.move_left(&MoveLeft, window, cx);
1619 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1620 editor.move_left(&MoveLeft, window, cx);
1621 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1622 });
1623}
1624
1625#[gpui::test]
1626fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1627 init_test(cx, |_| {});
1628
1629 let editor = cx.add_window(|window, cx| {
1630 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1631 build_editor(buffer, window, cx)
1632 });
1633 _ = editor.update(cx, |editor, window, cx| {
1634 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1635 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1636 });
1637
1638 // moving above start of document should move selection to start of document,
1639 // but the next move down should still be at the original goal_x
1640 editor.move_up(&MoveUp, window, cx);
1641 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1642
1643 editor.move_down(&MoveDown, window, cx);
1644 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "abcd".len())]);
1645
1646 editor.move_down(&MoveDown, window, cx);
1647 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1648
1649 editor.move_down(&MoveDown, window, cx);
1650 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1651
1652 editor.move_down(&MoveDown, window, cx);
1653 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1654
1655 // moving past end of document should not change goal_x
1656 editor.move_down(&MoveDown, window, cx);
1657 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1658
1659 editor.move_down(&MoveDown, window, cx);
1660 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1661
1662 editor.move_up(&MoveUp, window, cx);
1663 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1664
1665 editor.move_up(&MoveUp, window, cx);
1666 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1667
1668 editor.move_up(&MoveUp, window, cx);
1669 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1670 });
1671}
1672
1673#[gpui::test]
1674fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1675 init_test(cx, |_| {});
1676 let move_to_beg = MoveToBeginningOfLine {
1677 stop_at_soft_wraps: true,
1678 stop_at_indent: true,
1679 };
1680
1681 let delete_to_beg = DeleteToBeginningOfLine {
1682 stop_at_indent: false,
1683 };
1684
1685 let move_to_end = MoveToEndOfLine {
1686 stop_at_soft_wraps: true,
1687 };
1688
1689 let editor = cx.add_window(|window, cx| {
1690 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1691 build_editor(buffer, window, cx)
1692 });
1693 _ = editor.update(cx, |editor, window, cx| {
1694 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1695 s.select_display_ranges([
1696 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1697 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1698 ]);
1699 });
1700 });
1701
1702 _ = editor.update(cx, |editor, window, cx| {
1703 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1704 assert_eq!(
1705 display_ranges(editor, cx),
1706 &[
1707 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1708 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1709 ]
1710 );
1711 });
1712
1713 _ = editor.update(cx, |editor, window, cx| {
1714 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1715 assert_eq!(
1716 display_ranges(editor, cx),
1717 &[
1718 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1719 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1720 ]
1721 );
1722 });
1723
1724 _ = editor.update(cx, |editor, window, cx| {
1725 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1726 assert_eq!(
1727 display_ranges(editor, cx),
1728 &[
1729 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1730 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1731 ]
1732 );
1733 });
1734
1735 _ = editor.update(cx, |editor, window, cx| {
1736 editor.move_to_end_of_line(&move_to_end, window, cx);
1737 assert_eq!(
1738 display_ranges(editor, cx),
1739 &[
1740 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1741 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1742 ]
1743 );
1744 });
1745
1746 // Moving to the end of line again is a no-op.
1747 _ = editor.update(cx, |editor, window, cx| {
1748 editor.move_to_end_of_line(&move_to_end, window, cx);
1749 assert_eq!(
1750 display_ranges(editor, cx),
1751 &[
1752 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1753 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1754 ]
1755 );
1756 });
1757
1758 _ = editor.update(cx, |editor, window, cx| {
1759 editor.move_left(&MoveLeft, window, cx);
1760 editor.select_to_beginning_of_line(
1761 &SelectToBeginningOfLine {
1762 stop_at_soft_wraps: true,
1763 stop_at_indent: true,
1764 },
1765 window,
1766 cx,
1767 );
1768 assert_eq!(
1769 display_ranges(editor, cx),
1770 &[
1771 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1772 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1773 ]
1774 );
1775 });
1776
1777 _ = editor.update(cx, |editor, window, cx| {
1778 editor.select_to_beginning_of_line(
1779 &SelectToBeginningOfLine {
1780 stop_at_soft_wraps: true,
1781 stop_at_indent: true,
1782 },
1783 window,
1784 cx,
1785 );
1786 assert_eq!(
1787 display_ranges(editor, cx),
1788 &[
1789 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1790 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1791 ]
1792 );
1793 });
1794
1795 _ = editor.update(cx, |editor, window, cx| {
1796 editor.select_to_beginning_of_line(
1797 &SelectToBeginningOfLine {
1798 stop_at_soft_wraps: true,
1799 stop_at_indent: true,
1800 },
1801 window,
1802 cx,
1803 );
1804 assert_eq!(
1805 display_ranges(editor, cx),
1806 &[
1807 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1808 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1809 ]
1810 );
1811 });
1812
1813 _ = editor.update(cx, |editor, window, cx| {
1814 editor.select_to_end_of_line(
1815 &SelectToEndOfLine {
1816 stop_at_soft_wraps: true,
1817 },
1818 window,
1819 cx,
1820 );
1821 assert_eq!(
1822 display_ranges(editor, cx),
1823 &[
1824 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1825 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1826 ]
1827 );
1828 });
1829
1830 _ = editor.update(cx, |editor, window, cx| {
1831 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1832 assert_eq!(editor.display_text(cx), "ab\n de");
1833 assert_eq!(
1834 display_ranges(editor, cx),
1835 &[
1836 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1837 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1838 ]
1839 );
1840 });
1841
1842 _ = editor.update(cx, |editor, window, cx| {
1843 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1844 assert_eq!(editor.display_text(cx), "\n");
1845 assert_eq!(
1846 display_ranges(editor, cx),
1847 &[
1848 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1849 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1850 ]
1851 );
1852 });
1853}
1854
1855#[gpui::test]
1856fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1857 init_test(cx, |_| {});
1858 let move_to_beg = MoveToBeginningOfLine {
1859 stop_at_soft_wraps: false,
1860 stop_at_indent: false,
1861 };
1862
1863 let move_to_end = MoveToEndOfLine {
1864 stop_at_soft_wraps: false,
1865 };
1866
1867 let editor = cx.add_window(|window, cx| {
1868 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1869 build_editor(buffer, window, cx)
1870 });
1871
1872 _ = editor.update(cx, |editor, window, cx| {
1873 editor.set_wrap_width(Some(140.0.into()), cx);
1874
1875 // We expect the following lines after wrapping
1876 // ```
1877 // thequickbrownfox
1878 // jumpedoverthelazydo
1879 // gs
1880 // ```
1881 // The final `gs` was soft-wrapped onto a new line.
1882 assert_eq!(
1883 "thequickbrownfox\njumpedoverthelaz\nydogs",
1884 editor.display_text(cx),
1885 );
1886
1887 // First, let's assert behavior on the first line, that was not soft-wrapped.
1888 // Start the cursor at the `k` on the first line
1889 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1890 s.select_display_ranges([
1891 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1892 ]);
1893 });
1894
1895 // Moving to the beginning of the line should put us at the beginning of the line.
1896 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1897 assert_eq!(
1898 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1899 display_ranges(editor, cx)
1900 );
1901
1902 // Moving to the end of the line should put us at the end of the line.
1903 editor.move_to_end_of_line(&move_to_end, window, cx);
1904 assert_eq!(
1905 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1906 display_ranges(editor, cx)
1907 );
1908
1909 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1910 // Start the cursor at the last line (`y` that was wrapped to a new line)
1911 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1912 s.select_display_ranges([
1913 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1914 ]);
1915 });
1916
1917 // Moving to the beginning of the line should put us at the start of the second line of
1918 // display text, i.e., the `j`.
1919 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1920 assert_eq!(
1921 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1922 display_ranges(editor, cx)
1923 );
1924
1925 // Moving to the beginning of the line again should be a no-op.
1926 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1927 assert_eq!(
1928 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1929 display_ranges(editor, cx)
1930 );
1931
1932 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1933 // next display line.
1934 editor.move_to_end_of_line(&move_to_end, window, cx);
1935 assert_eq!(
1936 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1937 display_ranges(editor, cx)
1938 );
1939
1940 // Moving to the end of the line again should be a no-op.
1941 editor.move_to_end_of_line(&move_to_end, window, cx);
1942 assert_eq!(
1943 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1944 display_ranges(editor, cx)
1945 );
1946 });
1947}
1948
1949#[gpui::test]
1950fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1951 init_test(cx, |_| {});
1952
1953 let move_to_beg = MoveToBeginningOfLine {
1954 stop_at_soft_wraps: true,
1955 stop_at_indent: true,
1956 };
1957
1958 let select_to_beg = SelectToBeginningOfLine {
1959 stop_at_soft_wraps: true,
1960 stop_at_indent: true,
1961 };
1962
1963 let delete_to_beg = DeleteToBeginningOfLine {
1964 stop_at_indent: true,
1965 };
1966
1967 let move_to_end = MoveToEndOfLine {
1968 stop_at_soft_wraps: false,
1969 };
1970
1971 let editor = cx.add_window(|window, cx| {
1972 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1973 build_editor(buffer, window, cx)
1974 });
1975
1976 _ = editor.update(cx, |editor, window, cx| {
1977 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1978 s.select_display_ranges([
1979 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1980 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1981 ]);
1982 });
1983
1984 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1985 // and the second cursor at the first non-whitespace character in the line.
1986 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1987 assert_eq!(
1988 display_ranges(editor, cx),
1989 &[
1990 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1991 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1992 ]
1993 );
1994
1995 // Moving to the beginning of the line again should be a no-op for the first cursor,
1996 // and should move the second cursor to the beginning of the line.
1997 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1998 assert_eq!(
1999 display_ranges(editor, cx),
2000 &[
2001 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2002 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
2003 ]
2004 );
2005
2006 // Moving to the beginning of the line again should still be a no-op for the first cursor,
2007 // and should move the second cursor back to the first non-whitespace character in the line.
2008 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2009 assert_eq!(
2010 display_ranges(editor, cx),
2011 &[
2012 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2013 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2014 ]
2015 );
2016
2017 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
2018 // and to the first non-whitespace character in the line for the second cursor.
2019 editor.move_to_end_of_line(&move_to_end, window, cx);
2020 editor.move_left(&MoveLeft, window, cx);
2021 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2022 assert_eq!(
2023 display_ranges(editor, cx),
2024 &[
2025 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2026 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2027 ]
2028 );
2029
2030 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2031 // and should select to the beginning of the line for the second cursor.
2032 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2033 assert_eq!(
2034 display_ranges(editor, cx),
2035 &[
2036 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2037 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2038 ]
2039 );
2040
2041 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2042 // and should delete to the first non-whitespace character in the line for the second cursor.
2043 editor.move_to_end_of_line(&move_to_end, window, cx);
2044 editor.move_left(&MoveLeft, window, cx);
2045 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2046 assert_eq!(editor.text(cx), "c\n f");
2047 });
2048}
2049
2050#[gpui::test]
2051fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2052 init_test(cx, |_| {});
2053
2054 let move_to_beg = MoveToBeginningOfLine {
2055 stop_at_soft_wraps: true,
2056 stop_at_indent: true,
2057 };
2058
2059 let editor = cx.add_window(|window, cx| {
2060 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2061 build_editor(buffer, window, cx)
2062 });
2063
2064 _ = editor.update(cx, |editor, window, cx| {
2065 // test cursor between line_start and indent_start
2066 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2067 s.select_display_ranges([
2068 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2069 ]);
2070 });
2071
2072 // cursor should move to line_start
2073 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2074 assert_eq!(
2075 display_ranges(editor, cx),
2076 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2077 );
2078
2079 // cursor should move to indent_start
2080 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2081 assert_eq!(
2082 display_ranges(editor, cx),
2083 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2084 );
2085
2086 // cursor should move to back to line_start
2087 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2088 assert_eq!(
2089 display_ranges(editor, cx),
2090 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2091 );
2092 });
2093}
2094
2095#[gpui::test]
2096fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2097 init_test(cx, |_| {});
2098
2099 let editor = cx.add_window(|window, cx| {
2100 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2101 build_editor(buffer, window, cx)
2102 });
2103 _ = editor.update(cx, |editor, window, cx| {
2104 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2105 s.select_display_ranges([
2106 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2107 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2108 ])
2109 });
2110 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2111 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2112
2113 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2114 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
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_next_word_end(&MoveToNextWordEnd, window, cx);
2126 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2127
2128 editor.move_to_next_word_end(&MoveToNextWordEnd, 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_right(&MoveRight, window, cx);
2135 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2136 assert_selection_ranges(
2137 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2138 editor,
2139 cx,
2140 );
2141
2142 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2143 assert_selection_ranges(
2144 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2145 editor,
2146 cx,
2147 );
2148
2149 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2150 assert_selection_ranges(
2151 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2152 editor,
2153 cx,
2154 );
2155 });
2156}
2157
2158#[gpui::test]
2159fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2160 init_test(cx, |_| {});
2161
2162 let editor = cx.add_window(|window, cx| {
2163 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2164 build_editor(buffer, window, cx)
2165 });
2166
2167 _ = editor.update(cx, |editor, window, cx| {
2168 editor.set_wrap_width(Some(140.0.into()), cx);
2169 assert_eq!(
2170 editor.display_text(cx),
2171 "use one::{\n two::three::\n four::five\n};"
2172 );
2173
2174 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2175 s.select_display_ranges([
2176 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2177 ]);
2178 });
2179
2180 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2181 assert_eq!(
2182 display_ranges(editor, cx),
2183 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2184 );
2185
2186 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2187 assert_eq!(
2188 display_ranges(editor, cx),
2189 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2190 );
2191
2192 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2193 assert_eq!(
2194 display_ranges(editor, cx),
2195 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2196 );
2197
2198 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2199 assert_eq!(
2200 display_ranges(editor, cx),
2201 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2202 );
2203
2204 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2205 assert_eq!(
2206 display_ranges(editor, cx),
2207 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2208 );
2209
2210 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2211 assert_eq!(
2212 display_ranges(editor, cx),
2213 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2214 );
2215 });
2216}
2217
2218#[gpui::test]
2219async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2220 init_test(cx, |_| {});
2221 let mut cx = EditorTestContext::new(cx).await;
2222
2223 let line_height = cx.update_editor(|editor, window, cx| {
2224 editor
2225 .style(cx)
2226 .text
2227 .line_height_in_pixels(window.rem_size())
2228 });
2229 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2230
2231 cx.set_state(
2232 &r#"ˇone
2233 two
2234
2235 three
2236 fourˇ
2237 five
2238
2239 six"#
2240 .unindent(),
2241 );
2242
2243 cx.update_editor(|editor, window, cx| {
2244 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2245 });
2246 cx.assert_editor_state(
2247 &r#"one
2248 two
2249 ˇ
2250 three
2251 four
2252 five
2253 ˇ
2254 six"#
2255 .unindent(),
2256 );
2257
2258 cx.update_editor(|editor, window, cx| {
2259 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2260 });
2261 cx.assert_editor_state(
2262 &r#"one
2263 two
2264
2265 three
2266 four
2267 five
2268 ˇ
2269 sixˇ"#
2270 .unindent(),
2271 );
2272
2273 cx.update_editor(|editor, window, cx| {
2274 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2275 });
2276 cx.assert_editor_state(
2277 &r#"one
2278 two
2279
2280 three
2281 four
2282 five
2283
2284 sixˇ"#
2285 .unindent(),
2286 );
2287
2288 cx.update_editor(|editor, window, cx| {
2289 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2290 });
2291 cx.assert_editor_state(
2292 &r#"one
2293 two
2294
2295 three
2296 four
2297 five
2298 ˇ
2299 six"#
2300 .unindent(),
2301 );
2302
2303 cx.update_editor(|editor, window, cx| {
2304 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2305 });
2306 cx.assert_editor_state(
2307 &r#"one
2308 two
2309 ˇ
2310 three
2311 four
2312 five
2313
2314 six"#
2315 .unindent(),
2316 );
2317
2318 cx.update_editor(|editor, window, cx| {
2319 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2320 });
2321 cx.assert_editor_state(
2322 &r#"ˇone
2323 two
2324
2325 three
2326 four
2327 five
2328
2329 six"#
2330 .unindent(),
2331 );
2332}
2333
2334#[gpui::test]
2335async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2336 init_test(cx, |_| {});
2337 let mut cx = EditorTestContext::new(cx).await;
2338 let line_height = cx.update_editor(|editor, window, cx| {
2339 editor
2340 .style(cx)
2341 .text
2342 .line_height_in_pixels(window.rem_size())
2343 });
2344 let window = cx.window;
2345 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2346
2347 cx.set_state(
2348 r#"ˇone
2349 two
2350 three
2351 four
2352 five
2353 six
2354 seven
2355 eight
2356 nine
2357 ten
2358 "#,
2359 );
2360
2361 cx.update_editor(|editor, window, cx| {
2362 assert_eq!(
2363 editor.snapshot(window, cx).scroll_position(),
2364 gpui::Point::new(0., 0.)
2365 );
2366 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2367 assert_eq!(
2368 editor.snapshot(window, cx).scroll_position(),
2369 gpui::Point::new(0., 3.)
2370 );
2371 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2372 assert_eq!(
2373 editor.snapshot(window, cx).scroll_position(),
2374 gpui::Point::new(0., 6.)
2375 );
2376 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2377 assert_eq!(
2378 editor.snapshot(window, cx).scroll_position(),
2379 gpui::Point::new(0., 3.)
2380 );
2381
2382 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2383 assert_eq!(
2384 editor.snapshot(window, cx).scroll_position(),
2385 gpui::Point::new(0., 1.)
2386 );
2387 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2388 assert_eq!(
2389 editor.snapshot(window, cx).scroll_position(),
2390 gpui::Point::new(0., 3.)
2391 );
2392 });
2393}
2394
2395#[gpui::test]
2396async fn test_autoscroll(cx: &mut TestAppContext) {
2397 init_test(cx, |_| {});
2398 let mut cx = EditorTestContext::new(cx).await;
2399
2400 let line_height = cx.update_editor(|editor, window, cx| {
2401 editor.set_vertical_scroll_margin(2, cx);
2402 editor
2403 .style(cx)
2404 .text
2405 .line_height_in_pixels(window.rem_size())
2406 });
2407 let window = cx.window;
2408 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2409
2410 cx.set_state(
2411 r#"ˇone
2412 two
2413 three
2414 four
2415 five
2416 six
2417 seven
2418 eight
2419 nine
2420 ten
2421 "#,
2422 );
2423 cx.update_editor(|editor, window, cx| {
2424 assert_eq!(
2425 editor.snapshot(window, cx).scroll_position(),
2426 gpui::Point::new(0., 0.0)
2427 );
2428 });
2429
2430 // Add a cursor below the visible area. Since both cursors cannot fit
2431 // on screen, the editor autoscrolls to reveal the newest cursor, and
2432 // allows the vertical scroll margin below that cursor.
2433 cx.update_editor(|editor, window, cx| {
2434 editor.change_selections(Default::default(), window, cx, |selections| {
2435 selections.select_ranges([
2436 Point::new(0, 0)..Point::new(0, 0),
2437 Point::new(6, 0)..Point::new(6, 0),
2438 ]);
2439 })
2440 });
2441 cx.update_editor(|editor, window, cx| {
2442 assert_eq!(
2443 editor.snapshot(window, cx).scroll_position(),
2444 gpui::Point::new(0., 3.0)
2445 );
2446 });
2447
2448 // Move down. The editor cursor scrolls down to track the newest cursor.
2449 cx.update_editor(|editor, window, cx| {
2450 editor.move_down(&Default::default(), window, cx);
2451 });
2452 cx.update_editor(|editor, window, cx| {
2453 assert_eq!(
2454 editor.snapshot(window, cx).scroll_position(),
2455 gpui::Point::new(0., 4.0)
2456 );
2457 });
2458
2459 // Add a cursor above the visible area. Since both cursors fit on screen,
2460 // the editor scrolls to show both.
2461 cx.update_editor(|editor, window, cx| {
2462 editor.change_selections(Default::default(), window, cx, |selections| {
2463 selections.select_ranges([
2464 Point::new(1, 0)..Point::new(1, 0),
2465 Point::new(6, 0)..Point::new(6, 0),
2466 ]);
2467 })
2468 });
2469 cx.update_editor(|editor, window, cx| {
2470 assert_eq!(
2471 editor.snapshot(window, cx).scroll_position(),
2472 gpui::Point::new(0., 1.0)
2473 );
2474 });
2475}
2476
2477#[gpui::test]
2478async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2479 init_test(cx, |_| {});
2480 let mut cx = EditorTestContext::new(cx).await;
2481
2482 let line_height = cx.update_editor(|editor, window, cx| {
2483 editor
2484 .style(cx)
2485 .text
2486 .line_height_in_pixels(window.rem_size())
2487 });
2488 let window = cx.window;
2489 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2490 cx.set_state(
2491 &r#"
2492 ˇone
2493 two
2494 threeˇ
2495 four
2496 five
2497 six
2498 seven
2499 eight
2500 nine
2501 ten
2502 "#
2503 .unindent(),
2504 );
2505
2506 cx.update_editor(|editor, window, cx| {
2507 editor.move_page_down(&MovePageDown::default(), window, cx)
2508 });
2509 cx.assert_editor_state(
2510 &r#"
2511 one
2512 two
2513 three
2514 ˇfour
2515 five
2516 sixˇ
2517 seven
2518 eight
2519 nine
2520 ten
2521 "#
2522 .unindent(),
2523 );
2524
2525 cx.update_editor(|editor, window, cx| {
2526 editor.move_page_down(&MovePageDown::default(), window, cx)
2527 });
2528 cx.assert_editor_state(
2529 &r#"
2530 one
2531 two
2532 three
2533 four
2534 five
2535 six
2536 ˇseven
2537 eight
2538 nineˇ
2539 ten
2540 "#
2541 .unindent(),
2542 );
2543
2544 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2545 cx.assert_editor_state(
2546 &r#"
2547 one
2548 two
2549 three
2550 ˇfour
2551 five
2552 sixˇ
2553 seven
2554 eight
2555 nine
2556 ten
2557 "#
2558 .unindent(),
2559 );
2560
2561 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2562 cx.assert_editor_state(
2563 &r#"
2564 ˇone
2565 two
2566 threeˇ
2567 four
2568 five
2569 six
2570 seven
2571 eight
2572 nine
2573 ten
2574 "#
2575 .unindent(),
2576 );
2577
2578 // Test select collapsing
2579 cx.update_editor(|editor, window, cx| {
2580 editor.move_page_down(&MovePageDown::default(), window, cx);
2581 editor.move_page_down(&MovePageDown::default(), window, cx);
2582 editor.move_page_down(&MovePageDown::default(), window, cx);
2583 });
2584 cx.assert_editor_state(
2585 &r#"
2586 one
2587 two
2588 three
2589 four
2590 five
2591 six
2592 seven
2593 eight
2594 nine
2595 ˇten
2596 ˇ"#
2597 .unindent(),
2598 );
2599}
2600
2601#[gpui::test]
2602async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2603 init_test(cx, |_| {});
2604 let mut cx = EditorTestContext::new(cx).await;
2605 cx.set_state("one «two threeˇ» four");
2606 cx.update_editor(|editor, window, cx| {
2607 editor.delete_to_beginning_of_line(
2608 &DeleteToBeginningOfLine {
2609 stop_at_indent: false,
2610 },
2611 window,
2612 cx,
2613 );
2614 assert_eq!(editor.text(cx), " four");
2615 });
2616}
2617
2618#[gpui::test]
2619async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2620 init_test(cx, |_| {});
2621
2622 let mut cx = EditorTestContext::new(cx).await;
2623
2624 // For an empty selection, the preceding word fragment is deleted.
2625 // For non-empty selections, only selected characters are deleted.
2626 cx.set_state("onˇe two t«hreˇ»e four");
2627 cx.update_editor(|editor, window, cx| {
2628 editor.delete_to_previous_word_start(
2629 &DeleteToPreviousWordStart {
2630 ignore_newlines: false,
2631 ignore_brackets: false,
2632 },
2633 window,
2634 cx,
2635 );
2636 });
2637 cx.assert_editor_state("ˇe two tˇe four");
2638
2639 cx.set_state("e tˇwo te «fˇ»our");
2640 cx.update_editor(|editor, window, cx| {
2641 editor.delete_to_next_word_end(
2642 &DeleteToNextWordEnd {
2643 ignore_newlines: false,
2644 ignore_brackets: false,
2645 },
2646 window,
2647 cx,
2648 );
2649 });
2650 cx.assert_editor_state("e tˇ te ˇour");
2651}
2652
2653#[gpui::test]
2654async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2655 init_test(cx, |_| {});
2656
2657 let mut cx = EditorTestContext::new(cx).await;
2658
2659 cx.set_state("here is some text ˇwith a space");
2660 cx.update_editor(|editor, window, cx| {
2661 editor.delete_to_previous_word_start(
2662 &DeleteToPreviousWordStart {
2663 ignore_newlines: false,
2664 ignore_brackets: true,
2665 },
2666 window,
2667 cx,
2668 );
2669 });
2670 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2671 cx.assert_editor_state("here is some textˇwith a space");
2672
2673 cx.set_state("here is some text ˇwith a space");
2674 cx.update_editor(|editor, window, cx| {
2675 editor.delete_to_previous_word_start(
2676 &DeleteToPreviousWordStart {
2677 ignore_newlines: false,
2678 ignore_brackets: false,
2679 },
2680 window,
2681 cx,
2682 );
2683 });
2684 cx.assert_editor_state("here is some textˇwith a space");
2685
2686 cx.set_state("here is some textˇ with a space");
2687 cx.update_editor(|editor, window, cx| {
2688 editor.delete_to_next_word_end(
2689 &DeleteToNextWordEnd {
2690 ignore_newlines: false,
2691 ignore_brackets: true,
2692 },
2693 window,
2694 cx,
2695 );
2696 });
2697 // Same happens in the other direction.
2698 cx.assert_editor_state("here is some textˇwith a space");
2699
2700 cx.set_state("here is some textˇ with a space");
2701 cx.update_editor(|editor, window, cx| {
2702 editor.delete_to_next_word_end(
2703 &DeleteToNextWordEnd {
2704 ignore_newlines: false,
2705 ignore_brackets: false,
2706 },
2707 window,
2708 cx,
2709 );
2710 });
2711 cx.assert_editor_state("here is some textˇwith a space");
2712
2713 cx.set_state("here is some textˇ with a space");
2714 cx.update_editor(|editor, window, cx| {
2715 editor.delete_to_next_word_end(
2716 &DeleteToNextWordEnd {
2717 ignore_newlines: true,
2718 ignore_brackets: false,
2719 },
2720 window,
2721 cx,
2722 );
2723 });
2724 cx.assert_editor_state("here is some textˇwith a space");
2725 cx.update_editor(|editor, window, cx| {
2726 editor.delete_to_previous_word_start(
2727 &DeleteToPreviousWordStart {
2728 ignore_newlines: true,
2729 ignore_brackets: false,
2730 },
2731 window,
2732 cx,
2733 );
2734 });
2735 cx.assert_editor_state("here is some ˇwith a space");
2736 cx.update_editor(|editor, window, cx| {
2737 editor.delete_to_previous_word_start(
2738 &DeleteToPreviousWordStart {
2739 ignore_newlines: true,
2740 ignore_brackets: false,
2741 },
2742 window,
2743 cx,
2744 );
2745 });
2746 // Single whitespaces are removed with the word behind them.
2747 cx.assert_editor_state("here is ˇwith a space");
2748 cx.update_editor(|editor, window, cx| {
2749 editor.delete_to_previous_word_start(
2750 &DeleteToPreviousWordStart {
2751 ignore_newlines: true,
2752 ignore_brackets: false,
2753 },
2754 window,
2755 cx,
2756 );
2757 });
2758 cx.assert_editor_state("here ˇwith a space");
2759 cx.update_editor(|editor, window, cx| {
2760 editor.delete_to_previous_word_start(
2761 &DeleteToPreviousWordStart {
2762 ignore_newlines: true,
2763 ignore_brackets: false,
2764 },
2765 window,
2766 cx,
2767 );
2768 });
2769 cx.assert_editor_state("ˇwith a space");
2770 cx.update_editor(|editor, window, cx| {
2771 editor.delete_to_previous_word_start(
2772 &DeleteToPreviousWordStart {
2773 ignore_newlines: true,
2774 ignore_brackets: false,
2775 },
2776 window,
2777 cx,
2778 );
2779 });
2780 cx.assert_editor_state("ˇwith a space");
2781 cx.update_editor(|editor, window, cx| {
2782 editor.delete_to_next_word_end(
2783 &DeleteToNextWordEnd {
2784 ignore_newlines: true,
2785 ignore_brackets: false,
2786 },
2787 window,
2788 cx,
2789 );
2790 });
2791 // Same happens in the other direction.
2792 cx.assert_editor_state("ˇ a space");
2793 cx.update_editor(|editor, window, cx| {
2794 editor.delete_to_next_word_end(
2795 &DeleteToNextWordEnd {
2796 ignore_newlines: true,
2797 ignore_brackets: false,
2798 },
2799 window,
2800 cx,
2801 );
2802 });
2803 cx.assert_editor_state("ˇ space");
2804 cx.update_editor(|editor, window, cx| {
2805 editor.delete_to_next_word_end(
2806 &DeleteToNextWordEnd {
2807 ignore_newlines: true,
2808 ignore_brackets: false,
2809 },
2810 window,
2811 cx,
2812 );
2813 });
2814 cx.assert_editor_state("ˇ");
2815 cx.update_editor(|editor, window, cx| {
2816 editor.delete_to_next_word_end(
2817 &DeleteToNextWordEnd {
2818 ignore_newlines: true,
2819 ignore_brackets: false,
2820 },
2821 window,
2822 cx,
2823 );
2824 });
2825 cx.assert_editor_state("ˇ");
2826 cx.update_editor(|editor, window, cx| {
2827 editor.delete_to_previous_word_start(
2828 &DeleteToPreviousWordStart {
2829 ignore_newlines: true,
2830 ignore_brackets: false,
2831 },
2832 window,
2833 cx,
2834 );
2835 });
2836 cx.assert_editor_state("ˇ");
2837}
2838
2839#[gpui::test]
2840async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2841 init_test(cx, |_| {});
2842
2843 let language = Arc::new(
2844 Language::new(
2845 LanguageConfig {
2846 brackets: BracketPairConfig {
2847 pairs: vec![
2848 BracketPair {
2849 start: "\"".to_string(),
2850 end: "\"".to_string(),
2851 close: true,
2852 surround: true,
2853 newline: false,
2854 },
2855 BracketPair {
2856 start: "(".to_string(),
2857 end: ")".to_string(),
2858 close: true,
2859 surround: true,
2860 newline: true,
2861 },
2862 ],
2863 ..BracketPairConfig::default()
2864 },
2865 ..LanguageConfig::default()
2866 },
2867 Some(tree_sitter_rust::LANGUAGE.into()),
2868 )
2869 .with_brackets_query(
2870 r#"
2871 ("(" @open ")" @close)
2872 ("\"" @open "\"" @close)
2873 "#,
2874 )
2875 .unwrap(),
2876 );
2877
2878 let mut cx = EditorTestContext::new(cx).await;
2879 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2880
2881 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2882 cx.update_editor(|editor, window, cx| {
2883 editor.delete_to_previous_word_start(
2884 &DeleteToPreviousWordStart {
2885 ignore_newlines: true,
2886 ignore_brackets: false,
2887 },
2888 window,
2889 cx,
2890 );
2891 });
2892 // Deletion stops before brackets if asked to not ignore them.
2893 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2894 cx.update_editor(|editor, window, cx| {
2895 editor.delete_to_previous_word_start(
2896 &DeleteToPreviousWordStart {
2897 ignore_newlines: true,
2898 ignore_brackets: false,
2899 },
2900 window,
2901 cx,
2902 );
2903 });
2904 // Deletion has to remove a single bracket and then stop again.
2905 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2906
2907 cx.update_editor(|editor, window, cx| {
2908 editor.delete_to_previous_word_start(
2909 &DeleteToPreviousWordStart {
2910 ignore_newlines: true,
2911 ignore_brackets: false,
2912 },
2913 window,
2914 cx,
2915 );
2916 });
2917 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2918
2919 cx.update_editor(|editor, window, cx| {
2920 editor.delete_to_previous_word_start(
2921 &DeleteToPreviousWordStart {
2922 ignore_newlines: true,
2923 ignore_brackets: false,
2924 },
2925 window,
2926 cx,
2927 );
2928 });
2929 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2930
2931 cx.update_editor(|editor, window, cx| {
2932 editor.delete_to_previous_word_start(
2933 &DeleteToPreviousWordStart {
2934 ignore_newlines: true,
2935 ignore_brackets: false,
2936 },
2937 window,
2938 cx,
2939 );
2940 });
2941 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2942
2943 cx.update_editor(|editor, window, cx| {
2944 editor.delete_to_next_word_end(
2945 &DeleteToNextWordEnd {
2946 ignore_newlines: true,
2947 ignore_brackets: false,
2948 },
2949 window,
2950 cx,
2951 );
2952 });
2953 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2954 cx.assert_editor_state(r#"ˇ");"#);
2955
2956 cx.update_editor(|editor, window, cx| {
2957 editor.delete_to_next_word_end(
2958 &DeleteToNextWordEnd {
2959 ignore_newlines: true,
2960 ignore_brackets: false,
2961 },
2962 window,
2963 cx,
2964 );
2965 });
2966 cx.assert_editor_state(r#"ˇ"#);
2967
2968 cx.update_editor(|editor, window, cx| {
2969 editor.delete_to_next_word_end(
2970 &DeleteToNextWordEnd {
2971 ignore_newlines: true,
2972 ignore_brackets: false,
2973 },
2974 window,
2975 cx,
2976 );
2977 });
2978 cx.assert_editor_state(r#"ˇ"#);
2979
2980 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2981 cx.update_editor(|editor, window, cx| {
2982 editor.delete_to_previous_word_start(
2983 &DeleteToPreviousWordStart {
2984 ignore_newlines: true,
2985 ignore_brackets: true,
2986 },
2987 window,
2988 cx,
2989 );
2990 });
2991 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2992}
2993
2994#[gpui::test]
2995fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2996 init_test(cx, |_| {});
2997
2998 let editor = cx.add_window(|window, cx| {
2999 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
3000 build_editor(buffer, window, cx)
3001 });
3002 let del_to_prev_word_start = DeleteToPreviousWordStart {
3003 ignore_newlines: false,
3004 ignore_brackets: false,
3005 };
3006 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
3007 ignore_newlines: true,
3008 ignore_brackets: false,
3009 };
3010
3011 _ = editor.update(cx, |editor, window, cx| {
3012 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3013 s.select_display_ranges([
3014 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
3015 ])
3016 });
3017 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3018 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
3019 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3020 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
3021 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3022 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
3023 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3024 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
3025 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3026 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3027 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3028 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3029 });
3030}
3031
3032#[gpui::test]
3033fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3034 init_test(cx, |_| {});
3035
3036 let editor = cx.add_window(|window, cx| {
3037 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3038 build_editor(buffer, window, cx)
3039 });
3040 let del_to_next_word_end = DeleteToNextWordEnd {
3041 ignore_newlines: false,
3042 ignore_brackets: false,
3043 };
3044 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3045 ignore_newlines: true,
3046 ignore_brackets: false,
3047 };
3048
3049 _ = editor.update(cx, |editor, window, cx| {
3050 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3051 s.select_display_ranges([
3052 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3053 ])
3054 });
3055 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3056 assert_eq!(
3057 editor.buffer.read(cx).read(cx).text(),
3058 "one\n two\nthree\n four"
3059 );
3060 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3061 assert_eq!(
3062 editor.buffer.read(cx).read(cx).text(),
3063 "\n two\nthree\n four"
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 "two\nthree\n four"
3069 );
3070 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3071 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3072 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3073 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3074 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3075 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3076 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3077 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3078 });
3079}
3080
3081#[gpui::test]
3082fn test_newline(cx: &mut TestAppContext) {
3083 init_test(cx, |_| {});
3084
3085 let editor = cx.add_window(|window, cx| {
3086 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3087 build_editor(buffer, window, cx)
3088 });
3089
3090 _ = editor.update(cx, |editor, window, cx| {
3091 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3092 s.select_display_ranges([
3093 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3094 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3095 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3096 ])
3097 });
3098
3099 editor.newline(&Newline, window, cx);
3100 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3101 });
3102}
3103
3104#[gpui::test]
3105async fn test_newline_yaml(cx: &mut TestAppContext) {
3106 init_test(cx, |_| {});
3107
3108 let mut cx = EditorTestContext::new(cx).await;
3109 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3110 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3111
3112 // Object (between 2 fields)
3113 cx.set_state(indoc! {"
3114 test:ˇ
3115 hello: bye"});
3116 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3117 cx.assert_editor_state(indoc! {"
3118 test:
3119 ˇ
3120 hello: bye"});
3121
3122 // Object (first and single line)
3123 cx.set_state(indoc! {"
3124 test:ˇ"});
3125 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3126 cx.assert_editor_state(indoc! {"
3127 test:
3128 ˇ"});
3129
3130 // Array with objects (after first element)
3131 cx.set_state(indoc! {"
3132 test:
3133 - foo: barˇ"});
3134 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3135 cx.assert_editor_state(indoc! {"
3136 test:
3137 - foo: bar
3138 ˇ"});
3139
3140 // Array with objects and comment
3141 cx.set_state(indoc! {"
3142 test:
3143 - foo: bar
3144 - bar: # testˇ"});
3145 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3146 cx.assert_editor_state(indoc! {"
3147 test:
3148 - foo: bar
3149 - bar: # test
3150 ˇ"});
3151
3152 // Array with objects (after second element)
3153 cx.set_state(indoc! {"
3154 test:
3155 - foo: bar
3156 - bar: fooˇ"});
3157 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3158 cx.assert_editor_state(indoc! {"
3159 test:
3160 - foo: bar
3161 - bar: foo
3162 ˇ"});
3163
3164 // Array with strings (after first element)
3165 cx.set_state(indoc! {"
3166 test:
3167 - fooˇ"});
3168 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3169 cx.assert_editor_state(indoc! {"
3170 test:
3171 - foo
3172 ˇ"});
3173}
3174
3175#[gpui::test]
3176fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3177 init_test(cx, |_| {});
3178
3179 let editor = cx.add_window(|window, cx| {
3180 let buffer = MultiBuffer::build_simple(
3181 "
3182 a
3183 b(
3184 X
3185 )
3186 c(
3187 X
3188 )
3189 "
3190 .unindent()
3191 .as_str(),
3192 cx,
3193 );
3194 let mut editor = build_editor(buffer, window, cx);
3195 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3196 s.select_ranges([
3197 Point::new(2, 4)..Point::new(2, 5),
3198 Point::new(5, 4)..Point::new(5, 5),
3199 ])
3200 });
3201 editor
3202 });
3203
3204 _ = editor.update(cx, |editor, window, cx| {
3205 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3206 editor.buffer.update(cx, |buffer, cx| {
3207 buffer.edit(
3208 [
3209 (Point::new(1, 2)..Point::new(3, 0), ""),
3210 (Point::new(4, 2)..Point::new(6, 0), ""),
3211 ],
3212 None,
3213 cx,
3214 );
3215 assert_eq!(
3216 buffer.read(cx).text(),
3217 "
3218 a
3219 b()
3220 c()
3221 "
3222 .unindent()
3223 );
3224 });
3225 assert_eq!(
3226 editor.selections.ranges(&editor.display_snapshot(cx)),
3227 &[
3228 Point::new(1, 2)..Point::new(1, 2),
3229 Point::new(2, 2)..Point::new(2, 2),
3230 ],
3231 );
3232
3233 editor.newline(&Newline, window, cx);
3234 assert_eq!(
3235 editor.text(cx),
3236 "
3237 a
3238 b(
3239 )
3240 c(
3241 )
3242 "
3243 .unindent()
3244 );
3245
3246 // The selections are moved after the inserted newlines
3247 assert_eq!(
3248 editor.selections.ranges(&editor.display_snapshot(cx)),
3249 &[
3250 Point::new(2, 0)..Point::new(2, 0),
3251 Point::new(4, 0)..Point::new(4, 0),
3252 ],
3253 );
3254 });
3255}
3256
3257#[gpui::test]
3258async fn test_newline_above(cx: &mut TestAppContext) {
3259 init_test(cx, |settings| {
3260 settings.defaults.tab_size = NonZeroU32::new(4)
3261 });
3262
3263 let language = Arc::new(
3264 Language::new(
3265 LanguageConfig::default(),
3266 Some(tree_sitter_rust::LANGUAGE.into()),
3267 )
3268 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3269 .unwrap(),
3270 );
3271
3272 let mut cx = EditorTestContext::new(cx).await;
3273 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3274 cx.set_state(indoc! {"
3275 const a: ˇA = (
3276 (ˇ
3277 «const_functionˇ»(ˇ),
3278 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3279 )ˇ
3280 ˇ);ˇ
3281 "});
3282
3283 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3284 cx.assert_editor_state(indoc! {"
3285 ˇ
3286 const a: A = (
3287 ˇ
3288 (
3289 ˇ
3290 ˇ
3291 const_function(),
3292 ˇ
3293 ˇ
3294 ˇ
3295 ˇ
3296 something_else,
3297 ˇ
3298 )
3299 ˇ
3300 ˇ
3301 );
3302 "});
3303}
3304
3305#[gpui::test]
3306async fn test_newline_below(cx: &mut TestAppContext) {
3307 init_test(cx, |settings| {
3308 settings.defaults.tab_size = NonZeroU32::new(4)
3309 });
3310
3311 let language = Arc::new(
3312 Language::new(
3313 LanguageConfig::default(),
3314 Some(tree_sitter_rust::LANGUAGE.into()),
3315 )
3316 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3317 .unwrap(),
3318 );
3319
3320 let mut cx = EditorTestContext::new(cx).await;
3321 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3322 cx.set_state(indoc! {"
3323 const a: ˇA = (
3324 (ˇ
3325 «const_functionˇ»(ˇ),
3326 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3327 )ˇ
3328 ˇ);ˇ
3329 "});
3330
3331 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3332 cx.assert_editor_state(indoc! {"
3333 const a: A = (
3334 ˇ
3335 (
3336 ˇ
3337 const_function(),
3338 ˇ
3339 ˇ
3340 something_else,
3341 ˇ
3342 ˇ
3343 ˇ
3344 ˇ
3345 )
3346 ˇ
3347 );
3348 ˇ
3349 ˇ
3350 "});
3351}
3352
3353#[gpui::test]
3354async fn test_newline_comments(cx: &mut TestAppContext) {
3355 init_test(cx, |settings| {
3356 settings.defaults.tab_size = NonZeroU32::new(4)
3357 });
3358
3359 let language = Arc::new(Language::new(
3360 LanguageConfig {
3361 line_comments: vec!["// ".into()],
3362 ..LanguageConfig::default()
3363 },
3364 None,
3365 ));
3366 {
3367 let mut cx = EditorTestContext::new(cx).await;
3368 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3369 cx.set_state(indoc! {"
3370 // Fooˇ
3371 "});
3372
3373 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3374 cx.assert_editor_state(indoc! {"
3375 // Foo
3376 // ˇ
3377 "});
3378 // Ensure that we add comment prefix when existing line contains space
3379 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3380 cx.assert_editor_state(
3381 indoc! {"
3382 // Foo
3383 //s
3384 // ˇ
3385 "}
3386 .replace("s", " ") // s is used as space placeholder to prevent format on save
3387 .as_str(),
3388 );
3389 // Ensure that we add comment prefix when existing line does not contain space
3390 cx.set_state(indoc! {"
3391 // Foo
3392 //ˇ
3393 "});
3394 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3395 cx.assert_editor_state(indoc! {"
3396 // Foo
3397 //
3398 // ˇ
3399 "});
3400 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3401 cx.set_state(indoc! {"
3402 ˇ// Foo
3403 "});
3404 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3405 cx.assert_editor_state(indoc! {"
3406
3407 ˇ// Foo
3408 "});
3409 }
3410 // Ensure that comment continuations can be disabled.
3411 update_test_language_settings(cx, |settings| {
3412 settings.defaults.extend_comment_on_newline = Some(false);
3413 });
3414 let mut cx = EditorTestContext::new(cx).await;
3415 cx.set_state(indoc! {"
3416 // Fooˇ
3417 "});
3418 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3419 cx.assert_editor_state(indoc! {"
3420 // Foo
3421 ˇ
3422 "});
3423}
3424
3425#[gpui::test]
3426async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3427 init_test(cx, |settings| {
3428 settings.defaults.tab_size = NonZeroU32::new(4)
3429 });
3430
3431 let language = Arc::new(Language::new(
3432 LanguageConfig {
3433 line_comments: vec!["// ".into(), "/// ".into()],
3434 ..LanguageConfig::default()
3435 },
3436 None,
3437 ));
3438 {
3439 let mut cx = EditorTestContext::new(cx).await;
3440 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3441 cx.set_state(indoc! {"
3442 //ˇ
3443 "});
3444 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3445 cx.assert_editor_state(indoc! {"
3446 //
3447 // ˇ
3448 "});
3449
3450 cx.set_state(indoc! {"
3451 ///ˇ
3452 "});
3453 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3454 cx.assert_editor_state(indoc! {"
3455 ///
3456 /// ˇ
3457 "});
3458 }
3459}
3460
3461#[gpui::test]
3462async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3463 init_test(cx, |settings| {
3464 settings.defaults.tab_size = NonZeroU32::new(4)
3465 });
3466
3467 let language = Arc::new(
3468 Language::new(
3469 LanguageConfig {
3470 documentation_comment: Some(language::BlockCommentConfig {
3471 start: "/**".into(),
3472 end: "*/".into(),
3473 prefix: "* ".into(),
3474 tab_size: 1,
3475 }),
3476
3477 ..LanguageConfig::default()
3478 },
3479 Some(tree_sitter_rust::LANGUAGE.into()),
3480 )
3481 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3482 .unwrap(),
3483 );
3484
3485 {
3486 let mut cx = EditorTestContext::new(cx).await;
3487 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3488 cx.set_state(indoc! {"
3489 /**ˇ
3490 "});
3491
3492 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3493 cx.assert_editor_state(indoc! {"
3494 /**
3495 * ˇ
3496 "});
3497 // Ensure that if cursor is before the comment start,
3498 // we do not actually insert a comment prefix.
3499 cx.set_state(indoc! {"
3500 ˇ/**
3501 "});
3502 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3503 cx.assert_editor_state(indoc! {"
3504
3505 ˇ/**
3506 "});
3507 // Ensure that if cursor is between it doesn't add comment prefix.
3508 cx.set_state(indoc! {"
3509 /*ˇ*
3510 "});
3511 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3512 cx.assert_editor_state(indoc! {"
3513 /*
3514 ˇ*
3515 "});
3516 // Ensure that if suffix exists on same line after cursor it adds new line.
3517 cx.set_state(indoc! {"
3518 /**ˇ*/
3519 "});
3520 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3521 cx.assert_editor_state(indoc! {"
3522 /**
3523 * ˇ
3524 */
3525 "});
3526 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3527 cx.set_state(indoc! {"
3528 /**ˇ */
3529 "});
3530 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3531 cx.assert_editor_state(indoc! {"
3532 /**
3533 * ˇ
3534 */
3535 "});
3536 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3537 cx.set_state(indoc! {"
3538 /** ˇ*/
3539 "});
3540 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3541 cx.assert_editor_state(
3542 indoc! {"
3543 /**s
3544 * ˇ
3545 */
3546 "}
3547 .replace("s", " ") // s is used as space placeholder to prevent format on save
3548 .as_str(),
3549 );
3550 // Ensure that delimiter space is preserved when newline on already
3551 // spaced delimiter.
3552 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3553 cx.assert_editor_state(
3554 indoc! {"
3555 /**s
3556 *s
3557 * ˇ
3558 */
3559 "}
3560 .replace("s", " ") // s is used as space placeholder to prevent format on save
3561 .as_str(),
3562 );
3563 // Ensure that delimiter space is preserved when space is not
3564 // on existing delimiter.
3565 cx.set_state(indoc! {"
3566 /**
3567 *ˇ
3568 */
3569 "});
3570 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3571 cx.assert_editor_state(indoc! {"
3572 /**
3573 *
3574 * ˇ
3575 */
3576 "});
3577 // Ensure that if suffix exists on same line after cursor it
3578 // doesn't add extra new line if prefix is not on same line.
3579 cx.set_state(indoc! {"
3580 /**
3581 ˇ*/
3582 "});
3583 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3584 cx.assert_editor_state(indoc! {"
3585 /**
3586
3587 ˇ*/
3588 "});
3589 // Ensure that it detects suffix after existing prefix.
3590 cx.set_state(indoc! {"
3591 /**ˇ/
3592 "});
3593 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3594 cx.assert_editor_state(indoc! {"
3595 /**
3596 ˇ/
3597 "});
3598 // Ensure that if suffix exists on same line before
3599 // cursor it does not add comment prefix.
3600 cx.set_state(indoc! {"
3601 /** */ˇ
3602 "});
3603 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3604 cx.assert_editor_state(indoc! {"
3605 /** */
3606 ˇ
3607 "});
3608 // Ensure that if suffix exists on same line before
3609 // cursor it does not add comment prefix.
3610 cx.set_state(indoc! {"
3611 /**
3612 *
3613 */ˇ
3614 "});
3615 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3616 cx.assert_editor_state(indoc! {"
3617 /**
3618 *
3619 */
3620 ˇ
3621 "});
3622
3623 // Ensure that inline comment followed by code
3624 // doesn't add comment prefix on newline
3625 cx.set_state(indoc! {"
3626 /** */ textˇ
3627 "});
3628 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3629 cx.assert_editor_state(indoc! {"
3630 /** */ text
3631 ˇ
3632 "});
3633
3634 // Ensure that text after comment end tag
3635 // doesn't add comment prefix on newline
3636 cx.set_state(indoc! {"
3637 /**
3638 *
3639 */ˇtext
3640 "});
3641 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3642 cx.assert_editor_state(indoc! {"
3643 /**
3644 *
3645 */
3646 ˇtext
3647 "});
3648
3649 // Ensure if not comment block it doesn't
3650 // add comment prefix on newline
3651 cx.set_state(indoc! {"
3652 * textˇ
3653 "});
3654 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3655 cx.assert_editor_state(indoc! {"
3656 * text
3657 ˇ
3658 "});
3659 }
3660 // Ensure that comment continuations can be disabled.
3661 update_test_language_settings(cx, |settings| {
3662 settings.defaults.extend_comment_on_newline = Some(false);
3663 });
3664 let mut cx = EditorTestContext::new(cx).await;
3665 cx.set_state(indoc! {"
3666 /**ˇ
3667 "});
3668 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3669 cx.assert_editor_state(indoc! {"
3670 /**
3671 ˇ
3672 "});
3673}
3674
3675#[gpui::test]
3676async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3677 init_test(cx, |settings| {
3678 settings.defaults.tab_size = NonZeroU32::new(4)
3679 });
3680
3681 let lua_language = Arc::new(Language::new(
3682 LanguageConfig {
3683 line_comments: vec!["--".into()],
3684 block_comment: Some(language::BlockCommentConfig {
3685 start: "--[[".into(),
3686 prefix: "".into(),
3687 end: "]]".into(),
3688 tab_size: 0,
3689 }),
3690 ..LanguageConfig::default()
3691 },
3692 None,
3693 ));
3694
3695 let mut cx = EditorTestContext::new(cx).await;
3696 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3697
3698 // Line with line comment should extend
3699 cx.set_state(indoc! {"
3700 --ˇ
3701 "});
3702 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3703 cx.assert_editor_state(indoc! {"
3704 --
3705 --ˇ
3706 "});
3707
3708 // Line with block comment that matches line comment should not extend
3709 cx.set_state(indoc! {"
3710 --[[ˇ
3711 "});
3712 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3713 cx.assert_editor_state(indoc! {"
3714 --[[
3715 ˇ
3716 "});
3717}
3718
3719#[gpui::test]
3720fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3721 init_test(cx, |_| {});
3722
3723 let editor = cx.add_window(|window, cx| {
3724 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3725 let mut editor = build_editor(buffer, window, cx);
3726 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3727 s.select_ranges([
3728 MultiBufferOffset(3)..MultiBufferOffset(4),
3729 MultiBufferOffset(11)..MultiBufferOffset(12),
3730 MultiBufferOffset(19)..MultiBufferOffset(20),
3731 ])
3732 });
3733 editor
3734 });
3735
3736 _ = editor.update(cx, |editor, window, cx| {
3737 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3738 editor.buffer.update(cx, |buffer, cx| {
3739 buffer.edit(
3740 [
3741 (MultiBufferOffset(2)..MultiBufferOffset(5), ""),
3742 (MultiBufferOffset(10)..MultiBufferOffset(13), ""),
3743 (MultiBufferOffset(18)..MultiBufferOffset(21), ""),
3744 ],
3745 None,
3746 cx,
3747 );
3748 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3749 });
3750 assert_eq!(
3751 editor.selections.ranges(&editor.display_snapshot(cx)),
3752 &[
3753 MultiBufferOffset(2)..MultiBufferOffset(2),
3754 MultiBufferOffset(7)..MultiBufferOffset(7),
3755 MultiBufferOffset(12)..MultiBufferOffset(12)
3756 ],
3757 );
3758
3759 editor.insert("Z", window, cx);
3760 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3761
3762 // The selections are moved after the inserted characters
3763 assert_eq!(
3764 editor.selections.ranges(&editor.display_snapshot(cx)),
3765 &[
3766 MultiBufferOffset(3)..MultiBufferOffset(3),
3767 MultiBufferOffset(9)..MultiBufferOffset(9),
3768 MultiBufferOffset(15)..MultiBufferOffset(15)
3769 ],
3770 );
3771 });
3772}
3773
3774#[gpui::test]
3775async fn test_tab(cx: &mut TestAppContext) {
3776 init_test(cx, |settings| {
3777 settings.defaults.tab_size = NonZeroU32::new(3)
3778 });
3779
3780 let mut cx = EditorTestContext::new(cx).await;
3781 cx.set_state(indoc! {"
3782 ˇabˇc
3783 ˇ🏀ˇ🏀ˇefg
3784 dˇ
3785 "});
3786 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3787 cx.assert_editor_state(indoc! {"
3788 ˇab ˇc
3789 ˇ🏀 ˇ🏀 ˇefg
3790 d ˇ
3791 "});
3792
3793 cx.set_state(indoc! {"
3794 a
3795 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3796 "});
3797 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3798 cx.assert_editor_state(indoc! {"
3799 a
3800 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3801 "});
3802}
3803
3804#[gpui::test]
3805async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3806 init_test(cx, |_| {});
3807
3808 let mut cx = EditorTestContext::new(cx).await;
3809 let language = Arc::new(
3810 Language::new(
3811 LanguageConfig::default(),
3812 Some(tree_sitter_rust::LANGUAGE.into()),
3813 )
3814 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3815 .unwrap(),
3816 );
3817 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3818
3819 // test when all cursors are not at suggested indent
3820 // then simply move to their suggested indent location
3821 cx.set_state(indoc! {"
3822 const a: B = (
3823 c(
3824 ˇ
3825 ˇ )
3826 );
3827 "});
3828 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3829 cx.assert_editor_state(indoc! {"
3830 const a: B = (
3831 c(
3832 ˇ
3833 ˇ)
3834 );
3835 "});
3836
3837 // test cursor already at suggested indent not moving when
3838 // other cursors are yet to reach their suggested indents
3839 cx.set_state(indoc! {"
3840 ˇ
3841 const a: B = (
3842 c(
3843 d(
3844 ˇ
3845 )
3846 ˇ
3847 ˇ )
3848 );
3849 "});
3850 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3851 cx.assert_editor_state(indoc! {"
3852 ˇ
3853 const a: B = (
3854 c(
3855 d(
3856 ˇ
3857 )
3858 ˇ
3859 ˇ)
3860 );
3861 "});
3862 // test when all cursors are at suggested indent then tab is inserted
3863 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3864 cx.assert_editor_state(indoc! {"
3865 ˇ
3866 const a: B = (
3867 c(
3868 d(
3869 ˇ
3870 )
3871 ˇ
3872 ˇ)
3873 );
3874 "});
3875
3876 // test when current indent is less than suggested indent,
3877 // we adjust line to match suggested indent and move cursor to it
3878 //
3879 // when no other cursor is at word boundary, all of them should move
3880 cx.set_state(indoc! {"
3881 const a: B = (
3882 c(
3883 d(
3884 ˇ
3885 ˇ )
3886 ˇ )
3887 );
3888 "});
3889 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3890 cx.assert_editor_state(indoc! {"
3891 const a: B = (
3892 c(
3893 d(
3894 ˇ
3895 ˇ)
3896 ˇ)
3897 );
3898 "});
3899
3900 // test when current indent is less than suggested indent,
3901 // we adjust line to match suggested indent and move cursor to it
3902 //
3903 // when some other cursor is at word boundary, it should not move
3904 cx.set_state(indoc! {"
3905 const a: B = (
3906 c(
3907 d(
3908 ˇ
3909 ˇ )
3910 ˇ)
3911 );
3912 "});
3913 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3914 cx.assert_editor_state(indoc! {"
3915 const a: B = (
3916 c(
3917 d(
3918 ˇ
3919 ˇ)
3920 ˇ)
3921 );
3922 "});
3923
3924 // test when current indent is more than suggested indent,
3925 // we just move cursor to current indent instead of suggested indent
3926 //
3927 // when no other cursor is at word boundary, all of them should move
3928 cx.set_state(indoc! {"
3929 const a: B = (
3930 c(
3931 d(
3932 ˇ
3933 ˇ )
3934 ˇ )
3935 );
3936 "});
3937 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3938 cx.assert_editor_state(indoc! {"
3939 const a: B = (
3940 c(
3941 d(
3942 ˇ
3943 ˇ)
3944 ˇ)
3945 );
3946 "});
3947 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3948 cx.assert_editor_state(indoc! {"
3949 const a: B = (
3950 c(
3951 d(
3952 ˇ
3953 ˇ)
3954 ˇ)
3955 );
3956 "});
3957
3958 // test when current indent is more than suggested indent,
3959 // we just move cursor to current indent instead of suggested indent
3960 //
3961 // when some other cursor is at word boundary, it doesn't move
3962 cx.set_state(indoc! {"
3963 const a: B = (
3964 c(
3965 d(
3966 ˇ
3967 ˇ )
3968 ˇ)
3969 );
3970 "});
3971 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3972 cx.assert_editor_state(indoc! {"
3973 const a: B = (
3974 c(
3975 d(
3976 ˇ
3977 ˇ)
3978 ˇ)
3979 );
3980 "});
3981
3982 // handle auto-indent when there are multiple cursors on the same line
3983 cx.set_state(indoc! {"
3984 const a: B = (
3985 c(
3986 ˇ ˇ
3987 ˇ )
3988 );
3989 "});
3990 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3991 cx.assert_editor_state(indoc! {"
3992 const a: B = (
3993 c(
3994 ˇ
3995 ˇ)
3996 );
3997 "});
3998}
3999
4000#[gpui::test]
4001async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
4002 init_test(cx, |settings| {
4003 settings.defaults.tab_size = NonZeroU32::new(3)
4004 });
4005
4006 let mut cx = EditorTestContext::new(cx).await;
4007 cx.set_state(indoc! {"
4008 ˇ
4009 \t ˇ
4010 \t ˇ
4011 \t ˇ
4012 \t \t\t \t \t\t \t\t \t \t ˇ
4013 "});
4014
4015 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4016 cx.assert_editor_state(indoc! {"
4017 ˇ
4018 \t ˇ
4019 \t ˇ
4020 \t ˇ
4021 \t \t\t \t \t\t \t\t \t \t ˇ
4022 "});
4023}
4024
4025#[gpui::test]
4026async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
4027 init_test(cx, |settings| {
4028 settings.defaults.tab_size = NonZeroU32::new(4)
4029 });
4030
4031 let language = Arc::new(
4032 Language::new(
4033 LanguageConfig::default(),
4034 Some(tree_sitter_rust::LANGUAGE.into()),
4035 )
4036 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
4037 .unwrap(),
4038 );
4039
4040 let mut cx = EditorTestContext::new(cx).await;
4041 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4042 cx.set_state(indoc! {"
4043 fn a() {
4044 if b {
4045 \t ˇc
4046 }
4047 }
4048 "});
4049
4050 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4051 cx.assert_editor_state(indoc! {"
4052 fn a() {
4053 if b {
4054 ˇc
4055 }
4056 }
4057 "});
4058}
4059
4060#[gpui::test]
4061async fn test_indent_outdent(cx: &mut TestAppContext) {
4062 init_test(cx, |settings| {
4063 settings.defaults.tab_size = NonZeroU32::new(4);
4064 });
4065
4066 let mut cx = EditorTestContext::new(cx).await;
4067
4068 cx.set_state(indoc! {"
4069 «oneˇ» «twoˇ»
4070 three
4071 four
4072 "});
4073 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4074 cx.assert_editor_state(indoc! {"
4075 «oneˇ» «twoˇ»
4076 three
4077 four
4078 "});
4079
4080 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4081 cx.assert_editor_state(indoc! {"
4082 «oneˇ» «twoˇ»
4083 three
4084 four
4085 "});
4086
4087 // select across line ending
4088 cx.set_state(indoc! {"
4089 one two
4090 t«hree
4091 ˇ» four
4092 "});
4093 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4094 cx.assert_editor_state(indoc! {"
4095 one two
4096 t«hree
4097 ˇ» four
4098 "});
4099
4100 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4101 cx.assert_editor_state(indoc! {"
4102 one two
4103 t«hree
4104 ˇ» four
4105 "});
4106
4107 // Ensure that indenting/outdenting works when the cursor is at column 0.
4108 cx.set_state(indoc! {"
4109 one two
4110 ˇthree
4111 four
4112 "});
4113 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4114 cx.assert_editor_state(indoc! {"
4115 one two
4116 ˇthree
4117 four
4118 "});
4119
4120 cx.set_state(indoc! {"
4121 one two
4122 ˇ three
4123 four
4124 "});
4125 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4126 cx.assert_editor_state(indoc! {"
4127 one two
4128 ˇthree
4129 four
4130 "});
4131}
4132
4133#[gpui::test]
4134async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4135 // This is a regression test for issue #33761
4136 init_test(cx, |_| {});
4137
4138 let mut cx = EditorTestContext::new(cx).await;
4139 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4140 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4141
4142 cx.set_state(
4143 r#"ˇ# ingress:
4144ˇ# api:
4145ˇ# enabled: false
4146ˇ# pathType: Prefix
4147ˇ# console:
4148ˇ# enabled: false
4149ˇ# pathType: Prefix
4150"#,
4151 );
4152
4153 // Press tab to indent all lines
4154 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4155
4156 cx.assert_editor_state(
4157 r#" ˇ# ingress:
4158 ˇ# api:
4159 ˇ# enabled: false
4160 ˇ# pathType: Prefix
4161 ˇ# console:
4162 ˇ# enabled: false
4163 ˇ# pathType: Prefix
4164"#,
4165 );
4166}
4167
4168#[gpui::test]
4169async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4170 // This is a test to make sure our fix for issue #33761 didn't break anything
4171 init_test(cx, |_| {});
4172
4173 let mut cx = EditorTestContext::new(cx).await;
4174 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4175 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4176
4177 cx.set_state(
4178 r#"ˇingress:
4179ˇ api:
4180ˇ enabled: false
4181ˇ pathType: Prefix
4182"#,
4183 );
4184
4185 // Press tab to indent all lines
4186 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4187
4188 cx.assert_editor_state(
4189 r#"ˇingress:
4190 ˇapi:
4191 ˇenabled: false
4192 ˇpathType: Prefix
4193"#,
4194 );
4195}
4196
4197#[gpui::test]
4198async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4199 init_test(cx, |settings| {
4200 settings.defaults.hard_tabs = Some(true);
4201 });
4202
4203 let mut cx = EditorTestContext::new(cx).await;
4204
4205 // select two ranges on one line
4206 cx.set_state(indoc! {"
4207 «oneˇ» «twoˇ»
4208 three
4209 four
4210 "});
4211 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4212 cx.assert_editor_state(indoc! {"
4213 \t«oneˇ» «twoˇ»
4214 three
4215 four
4216 "});
4217 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4218 cx.assert_editor_state(indoc! {"
4219 \t\t«oneˇ» «twoˇ»
4220 three
4221 four
4222 "});
4223 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4224 cx.assert_editor_state(indoc! {"
4225 \t«oneˇ» «twoˇ»
4226 three
4227 four
4228 "});
4229 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4230 cx.assert_editor_state(indoc! {"
4231 «oneˇ» «twoˇ»
4232 three
4233 four
4234 "});
4235
4236 // select across a line ending
4237 cx.set_state(indoc! {"
4238 one two
4239 t«hree
4240 ˇ»four
4241 "});
4242 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4243 cx.assert_editor_state(indoc! {"
4244 one two
4245 \tt«hree
4246 ˇ»four
4247 "});
4248 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4249 cx.assert_editor_state(indoc! {"
4250 one two
4251 \t\tt«hree
4252 ˇ»four
4253 "});
4254 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4255 cx.assert_editor_state(indoc! {"
4256 one two
4257 \tt«hree
4258 ˇ»four
4259 "});
4260 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4261 cx.assert_editor_state(indoc! {"
4262 one two
4263 t«hree
4264 ˇ»four
4265 "});
4266
4267 // Ensure that indenting/outdenting works when the cursor is at column 0.
4268 cx.set_state(indoc! {"
4269 one two
4270 ˇthree
4271 four
4272 "});
4273 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4274 cx.assert_editor_state(indoc! {"
4275 one two
4276 ˇthree
4277 four
4278 "});
4279 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4280 cx.assert_editor_state(indoc! {"
4281 one two
4282 \tˇthree
4283 four
4284 "});
4285 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4286 cx.assert_editor_state(indoc! {"
4287 one two
4288 ˇthree
4289 four
4290 "});
4291}
4292
4293#[gpui::test]
4294fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4295 init_test(cx, |settings| {
4296 settings.languages.0.extend([
4297 (
4298 "TOML".into(),
4299 LanguageSettingsContent {
4300 tab_size: NonZeroU32::new(2),
4301 ..Default::default()
4302 },
4303 ),
4304 (
4305 "Rust".into(),
4306 LanguageSettingsContent {
4307 tab_size: NonZeroU32::new(4),
4308 ..Default::default()
4309 },
4310 ),
4311 ]);
4312 });
4313
4314 let toml_language = Arc::new(Language::new(
4315 LanguageConfig {
4316 name: "TOML".into(),
4317 ..Default::default()
4318 },
4319 None,
4320 ));
4321 let rust_language = Arc::new(Language::new(
4322 LanguageConfig {
4323 name: "Rust".into(),
4324 ..Default::default()
4325 },
4326 None,
4327 ));
4328
4329 let toml_buffer =
4330 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4331 let rust_buffer =
4332 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4333 let multibuffer = cx.new(|cx| {
4334 let mut multibuffer = MultiBuffer::new(ReadWrite);
4335 multibuffer.push_excerpts(
4336 toml_buffer.clone(),
4337 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4338 cx,
4339 );
4340 multibuffer.push_excerpts(
4341 rust_buffer.clone(),
4342 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4343 cx,
4344 );
4345 multibuffer
4346 });
4347
4348 cx.add_window(|window, cx| {
4349 let mut editor = build_editor(multibuffer, window, cx);
4350
4351 assert_eq!(
4352 editor.text(cx),
4353 indoc! {"
4354 a = 1
4355 b = 2
4356
4357 const c: usize = 3;
4358 "}
4359 );
4360
4361 select_ranges(
4362 &mut editor,
4363 indoc! {"
4364 «aˇ» = 1
4365 b = 2
4366
4367 «const c:ˇ» usize = 3;
4368 "},
4369 window,
4370 cx,
4371 );
4372
4373 editor.tab(&Tab, window, cx);
4374 assert_text_with_selections(
4375 &mut editor,
4376 indoc! {"
4377 «aˇ» = 1
4378 b = 2
4379
4380 «const c:ˇ» usize = 3;
4381 "},
4382 cx,
4383 );
4384 editor.backtab(&Backtab, window, cx);
4385 assert_text_with_selections(
4386 &mut editor,
4387 indoc! {"
4388 «aˇ» = 1
4389 b = 2
4390
4391 «const c:ˇ» usize = 3;
4392 "},
4393 cx,
4394 );
4395
4396 editor
4397 });
4398}
4399
4400#[gpui::test]
4401async fn test_backspace(cx: &mut TestAppContext) {
4402 init_test(cx, |_| {});
4403
4404 let mut cx = EditorTestContext::new(cx).await;
4405
4406 // Basic backspace
4407 cx.set_state(indoc! {"
4408 onˇe two three
4409 fou«rˇ» five six
4410 seven «ˇeight nine
4411 »ten
4412 "});
4413 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4414 cx.assert_editor_state(indoc! {"
4415 oˇe two three
4416 fouˇ five six
4417 seven ˇten
4418 "});
4419
4420 // Test backspace inside and around indents
4421 cx.set_state(indoc! {"
4422 zero
4423 ˇone
4424 ˇtwo
4425 ˇ ˇ ˇ three
4426 ˇ ˇ four
4427 "});
4428 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4429 cx.assert_editor_state(indoc! {"
4430 zero
4431 ˇone
4432 ˇtwo
4433 ˇ threeˇ four
4434 "});
4435}
4436
4437#[gpui::test]
4438async fn test_delete(cx: &mut TestAppContext) {
4439 init_test(cx, |_| {});
4440
4441 let mut cx = EditorTestContext::new(cx).await;
4442 cx.set_state(indoc! {"
4443 onˇe two three
4444 fou«rˇ» five six
4445 seven «ˇeight nine
4446 »ten
4447 "});
4448 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4449 cx.assert_editor_state(indoc! {"
4450 onˇ two three
4451 fouˇ five six
4452 seven ˇten
4453 "});
4454}
4455
4456#[gpui::test]
4457fn test_delete_line(cx: &mut TestAppContext) {
4458 init_test(cx, |_| {});
4459
4460 let editor = cx.add_window(|window, cx| {
4461 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4462 build_editor(buffer, window, cx)
4463 });
4464 _ = editor.update(cx, |editor, window, cx| {
4465 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4466 s.select_display_ranges([
4467 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4468 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4469 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4470 ])
4471 });
4472 editor.delete_line(&DeleteLine, window, cx);
4473 assert_eq!(editor.display_text(cx), "ghi");
4474 assert_eq!(
4475 display_ranges(editor, cx),
4476 vec![
4477 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4478 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4479 ]
4480 );
4481 });
4482
4483 let editor = cx.add_window(|window, cx| {
4484 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4485 build_editor(buffer, window, cx)
4486 });
4487 _ = editor.update(cx, |editor, window, cx| {
4488 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4489 s.select_display_ranges([
4490 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4491 ])
4492 });
4493 editor.delete_line(&DeleteLine, window, cx);
4494 assert_eq!(editor.display_text(cx), "ghi\n");
4495 assert_eq!(
4496 display_ranges(editor, cx),
4497 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4498 );
4499 });
4500
4501 let editor = cx.add_window(|window, cx| {
4502 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4503 build_editor(buffer, window, cx)
4504 });
4505 _ = editor.update(cx, |editor, window, cx| {
4506 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4507 s.select_display_ranges([
4508 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4509 ])
4510 });
4511 editor.delete_line(&DeleteLine, window, cx);
4512 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4513 assert_eq!(
4514 display_ranges(editor, cx),
4515 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4516 );
4517 });
4518}
4519
4520#[gpui::test]
4521fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4522 init_test(cx, |_| {});
4523
4524 cx.add_window(|window, cx| {
4525 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4526 let mut editor = build_editor(buffer.clone(), window, cx);
4527 let buffer = buffer.read(cx).as_singleton().unwrap();
4528
4529 assert_eq!(
4530 editor
4531 .selections
4532 .ranges::<Point>(&editor.display_snapshot(cx)),
4533 &[Point::new(0, 0)..Point::new(0, 0)]
4534 );
4535
4536 // When on single line, replace newline at end by space
4537 editor.join_lines(&JoinLines, window, cx);
4538 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4539 assert_eq!(
4540 editor
4541 .selections
4542 .ranges::<Point>(&editor.display_snapshot(cx)),
4543 &[Point::new(0, 3)..Point::new(0, 3)]
4544 );
4545
4546 // When multiple lines are selected, remove newlines that are spanned by the selection
4547 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4548 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4549 });
4550 editor.join_lines(&JoinLines, window, cx);
4551 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4552 assert_eq!(
4553 editor
4554 .selections
4555 .ranges::<Point>(&editor.display_snapshot(cx)),
4556 &[Point::new(0, 11)..Point::new(0, 11)]
4557 );
4558
4559 // Undo should be transactional
4560 editor.undo(&Undo, window, cx);
4561 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4562 assert_eq!(
4563 editor
4564 .selections
4565 .ranges::<Point>(&editor.display_snapshot(cx)),
4566 &[Point::new(0, 5)..Point::new(2, 2)]
4567 );
4568
4569 // When joining an empty line don't insert a space
4570 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4571 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4572 });
4573 editor.join_lines(&JoinLines, window, cx);
4574 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4575 assert_eq!(
4576 editor
4577 .selections
4578 .ranges::<Point>(&editor.display_snapshot(cx)),
4579 [Point::new(2, 3)..Point::new(2, 3)]
4580 );
4581
4582 // We can remove trailing newlines
4583 editor.join_lines(&JoinLines, window, cx);
4584 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4585 assert_eq!(
4586 editor
4587 .selections
4588 .ranges::<Point>(&editor.display_snapshot(cx)),
4589 [Point::new(2, 3)..Point::new(2, 3)]
4590 );
4591
4592 // We don't blow up on the last line
4593 editor.join_lines(&JoinLines, window, cx);
4594 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4595 assert_eq!(
4596 editor
4597 .selections
4598 .ranges::<Point>(&editor.display_snapshot(cx)),
4599 [Point::new(2, 3)..Point::new(2, 3)]
4600 );
4601
4602 // reset to test indentation
4603 editor.buffer.update(cx, |buffer, cx| {
4604 buffer.edit(
4605 [
4606 (Point::new(1, 0)..Point::new(1, 2), " "),
4607 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4608 ],
4609 None,
4610 cx,
4611 )
4612 });
4613
4614 // We remove any leading spaces
4615 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4616 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4617 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4618 });
4619 editor.join_lines(&JoinLines, window, cx);
4620 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4621
4622 // We don't insert a space for a line containing only spaces
4623 editor.join_lines(&JoinLines, window, cx);
4624 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4625
4626 // We ignore any leading tabs
4627 editor.join_lines(&JoinLines, window, cx);
4628 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4629
4630 editor
4631 });
4632}
4633
4634#[gpui::test]
4635fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4636 init_test(cx, |_| {});
4637
4638 cx.add_window(|window, cx| {
4639 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4640 let mut editor = build_editor(buffer.clone(), window, cx);
4641 let buffer = buffer.read(cx).as_singleton().unwrap();
4642
4643 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4644 s.select_ranges([
4645 Point::new(0, 2)..Point::new(1, 1),
4646 Point::new(1, 2)..Point::new(1, 2),
4647 Point::new(3, 1)..Point::new(3, 2),
4648 ])
4649 });
4650
4651 editor.join_lines(&JoinLines, window, cx);
4652 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4653
4654 assert_eq!(
4655 editor
4656 .selections
4657 .ranges::<Point>(&editor.display_snapshot(cx)),
4658 [
4659 Point::new(0, 7)..Point::new(0, 7),
4660 Point::new(1, 3)..Point::new(1, 3)
4661 ]
4662 );
4663 editor
4664 });
4665}
4666
4667#[gpui::test]
4668async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4669 init_test(cx, |_| {});
4670
4671 let mut cx = EditorTestContext::new(cx).await;
4672
4673 let diff_base = r#"
4674 Line 0
4675 Line 1
4676 Line 2
4677 Line 3
4678 "#
4679 .unindent();
4680
4681 cx.set_state(
4682 &r#"
4683 ˇLine 0
4684 Line 1
4685 Line 2
4686 Line 3
4687 "#
4688 .unindent(),
4689 );
4690
4691 cx.set_head_text(&diff_base);
4692 executor.run_until_parked();
4693
4694 // Join lines
4695 cx.update_editor(|editor, window, cx| {
4696 editor.join_lines(&JoinLines, window, cx);
4697 });
4698 executor.run_until_parked();
4699
4700 cx.assert_editor_state(
4701 &r#"
4702 Line 0ˇ Line 1
4703 Line 2
4704 Line 3
4705 "#
4706 .unindent(),
4707 );
4708 // Join again
4709 cx.update_editor(|editor, window, cx| {
4710 editor.join_lines(&JoinLines, window, cx);
4711 });
4712 executor.run_until_parked();
4713
4714 cx.assert_editor_state(
4715 &r#"
4716 Line 0 Line 1ˇ Line 2
4717 Line 3
4718 "#
4719 .unindent(),
4720 );
4721}
4722
4723#[gpui::test]
4724async fn test_custom_newlines_cause_no_false_positive_diffs(
4725 executor: BackgroundExecutor,
4726 cx: &mut TestAppContext,
4727) {
4728 init_test(cx, |_| {});
4729 let mut cx = EditorTestContext::new(cx).await;
4730 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4731 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4732 executor.run_until_parked();
4733
4734 cx.update_editor(|editor, window, cx| {
4735 let snapshot = editor.snapshot(window, cx);
4736 assert_eq!(
4737 snapshot
4738 .buffer_snapshot()
4739 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
4740 .collect::<Vec<_>>(),
4741 Vec::new(),
4742 "Should not have any diffs for files with custom newlines"
4743 );
4744 });
4745}
4746
4747#[gpui::test]
4748async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4749 init_test(cx, |_| {});
4750
4751 let mut cx = EditorTestContext::new(cx).await;
4752
4753 // Test sort_lines_case_insensitive()
4754 cx.set_state(indoc! {"
4755 «z
4756 y
4757 x
4758 Z
4759 Y
4760 Xˇ»
4761 "});
4762 cx.update_editor(|e, window, cx| {
4763 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4764 });
4765 cx.assert_editor_state(indoc! {"
4766 «x
4767 X
4768 y
4769 Y
4770 z
4771 Zˇ»
4772 "});
4773
4774 // Test sort_lines_by_length()
4775 //
4776 // Demonstrates:
4777 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4778 // - sort is stable
4779 cx.set_state(indoc! {"
4780 «123
4781 æ
4782 12
4783 ∞
4784 1
4785 æˇ»
4786 "});
4787 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4788 cx.assert_editor_state(indoc! {"
4789 «æ
4790 ∞
4791 1
4792 æ
4793 12
4794 123ˇ»
4795 "});
4796
4797 // Test reverse_lines()
4798 cx.set_state(indoc! {"
4799 «5
4800 4
4801 3
4802 2
4803 1ˇ»
4804 "});
4805 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4806 cx.assert_editor_state(indoc! {"
4807 «1
4808 2
4809 3
4810 4
4811 5ˇ»
4812 "});
4813
4814 // Skip testing shuffle_line()
4815
4816 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4817 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4818
4819 // Don't manipulate when cursor is on single line, but expand the selection
4820 cx.set_state(indoc! {"
4821 ddˇdd
4822 ccc
4823 bb
4824 a
4825 "});
4826 cx.update_editor(|e, window, cx| {
4827 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4828 });
4829 cx.assert_editor_state(indoc! {"
4830 «ddddˇ»
4831 ccc
4832 bb
4833 a
4834 "});
4835
4836 // Basic manipulate case
4837 // Start selection moves to column 0
4838 // End of selection shrinks to fit shorter line
4839 cx.set_state(indoc! {"
4840 dd«d
4841 ccc
4842 bb
4843 aaaaaˇ»
4844 "});
4845 cx.update_editor(|e, window, cx| {
4846 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4847 });
4848 cx.assert_editor_state(indoc! {"
4849 «aaaaa
4850 bb
4851 ccc
4852 dddˇ»
4853 "});
4854
4855 // Manipulate case with newlines
4856 cx.set_state(indoc! {"
4857 dd«d
4858 ccc
4859
4860 bb
4861 aaaaa
4862
4863 ˇ»
4864 "});
4865 cx.update_editor(|e, window, cx| {
4866 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4867 });
4868 cx.assert_editor_state(indoc! {"
4869 «
4870
4871 aaaaa
4872 bb
4873 ccc
4874 dddˇ»
4875
4876 "});
4877
4878 // Adding new line
4879 cx.set_state(indoc! {"
4880 aa«a
4881 bbˇ»b
4882 "});
4883 cx.update_editor(|e, window, cx| {
4884 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4885 });
4886 cx.assert_editor_state(indoc! {"
4887 «aaa
4888 bbb
4889 added_lineˇ»
4890 "});
4891
4892 // Removing line
4893 cx.set_state(indoc! {"
4894 aa«a
4895 bbbˇ»
4896 "});
4897 cx.update_editor(|e, window, cx| {
4898 e.manipulate_immutable_lines(window, cx, |lines| {
4899 lines.pop();
4900 })
4901 });
4902 cx.assert_editor_state(indoc! {"
4903 «aaaˇ»
4904 "});
4905
4906 // Removing all lines
4907 cx.set_state(indoc! {"
4908 aa«a
4909 bbbˇ»
4910 "});
4911 cx.update_editor(|e, window, cx| {
4912 e.manipulate_immutable_lines(window, cx, |lines| {
4913 lines.drain(..);
4914 })
4915 });
4916 cx.assert_editor_state(indoc! {"
4917 ˇ
4918 "});
4919}
4920
4921#[gpui::test]
4922async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4923 init_test(cx, |_| {});
4924
4925 let mut cx = EditorTestContext::new(cx).await;
4926
4927 // Consider continuous selection as single selection
4928 cx.set_state(indoc! {"
4929 Aaa«aa
4930 cˇ»c«c
4931 bb
4932 aaaˇ»aa
4933 "});
4934 cx.update_editor(|e, window, cx| {
4935 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4936 });
4937 cx.assert_editor_state(indoc! {"
4938 «Aaaaa
4939 ccc
4940 bb
4941 aaaaaˇ»
4942 "});
4943
4944 cx.set_state(indoc! {"
4945 Aaa«aa
4946 cˇ»c«c
4947 bb
4948 aaaˇ»aa
4949 "});
4950 cx.update_editor(|e, window, cx| {
4951 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4952 });
4953 cx.assert_editor_state(indoc! {"
4954 «Aaaaa
4955 ccc
4956 bbˇ»
4957 "});
4958
4959 // Consider non continuous selection as distinct dedup operations
4960 cx.set_state(indoc! {"
4961 «aaaaa
4962 bb
4963 aaaaa
4964 aaaaaˇ»
4965
4966 aaa«aaˇ»
4967 "});
4968 cx.update_editor(|e, window, cx| {
4969 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4970 });
4971 cx.assert_editor_state(indoc! {"
4972 «aaaaa
4973 bbˇ»
4974
4975 «aaaaaˇ»
4976 "});
4977}
4978
4979#[gpui::test]
4980async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4981 init_test(cx, |_| {});
4982
4983 let mut cx = EditorTestContext::new(cx).await;
4984
4985 cx.set_state(indoc! {"
4986 «Aaa
4987 aAa
4988 Aaaˇ»
4989 "});
4990 cx.update_editor(|e, window, cx| {
4991 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4992 });
4993 cx.assert_editor_state(indoc! {"
4994 «Aaa
4995 aAaˇ»
4996 "});
4997
4998 cx.set_state(indoc! {"
4999 «Aaa
5000 aAa
5001 aaAˇ»
5002 "});
5003 cx.update_editor(|e, window, cx| {
5004 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
5005 });
5006 cx.assert_editor_state(indoc! {"
5007 «Aaaˇ»
5008 "});
5009}
5010
5011#[gpui::test]
5012async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
5013 init_test(cx, |_| {});
5014
5015 let mut cx = EditorTestContext::new(cx).await;
5016
5017 let js_language = Arc::new(Language::new(
5018 LanguageConfig {
5019 name: "JavaScript".into(),
5020 wrap_characters: Some(language::WrapCharactersConfig {
5021 start_prefix: "<".into(),
5022 start_suffix: ">".into(),
5023 end_prefix: "</".into(),
5024 end_suffix: ">".into(),
5025 }),
5026 ..LanguageConfig::default()
5027 },
5028 None,
5029 ));
5030
5031 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5032
5033 cx.set_state(indoc! {"
5034 «testˇ»
5035 "});
5036 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5037 cx.assert_editor_state(indoc! {"
5038 <«ˇ»>test</«ˇ»>
5039 "});
5040
5041 cx.set_state(indoc! {"
5042 «test
5043 testˇ»
5044 "});
5045 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5046 cx.assert_editor_state(indoc! {"
5047 <«ˇ»>test
5048 test</«ˇ»>
5049 "});
5050
5051 cx.set_state(indoc! {"
5052 teˇst
5053 "});
5054 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5055 cx.assert_editor_state(indoc! {"
5056 te<«ˇ»></«ˇ»>st
5057 "});
5058}
5059
5060#[gpui::test]
5061async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5062 init_test(cx, |_| {});
5063
5064 let mut cx = EditorTestContext::new(cx).await;
5065
5066 let js_language = Arc::new(Language::new(
5067 LanguageConfig {
5068 name: "JavaScript".into(),
5069 wrap_characters: Some(language::WrapCharactersConfig {
5070 start_prefix: "<".into(),
5071 start_suffix: ">".into(),
5072 end_prefix: "</".into(),
5073 end_suffix: ">".into(),
5074 }),
5075 ..LanguageConfig::default()
5076 },
5077 None,
5078 ));
5079
5080 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5081
5082 cx.set_state(indoc! {"
5083 «testˇ»
5084 «testˇ» «testˇ»
5085 «testˇ»
5086 "});
5087 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5088 cx.assert_editor_state(indoc! {"
5089 <«ˇ»>test</«ˇ»>
5090 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5091 <«ˇ»>test</«ˇ»>
5092 "});
5093
5094 cx.set_state(indoc! {"
5095 «test
5096 testˇ»
5097 «test
5098 testˇ»
5099 "});
5100 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5101 cx.assert_editor_state(indoc! {"
5102 <«ˇ»>test
5103 test</«ˇ»>
5104 <«ˇ»>test
5105 test</«ˇ»>
5106 "});
5107}
5108
5109#[gpui::test]
5110async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5111 init_test(cx, |_| {});
5112
5113 let mut cx = EditorTestContext::new(cx).await;
5114
5115 let plaintext_language = Arc::new(Language::new(
5116 LanguageConfig {
5117 name: "Plain Text".into(),
5118 ..LanguageConfig::default()
5119 },
5120 None,
5121 ));
5122
5123 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5124
5125 cx.set_state(indoc! {"
5126 «testˇ»
5127 "});
5128 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5129 cx.assert_editor_state(indoc! {"
5130 «testˇ»
5131 "});
5132}
5133
5134#[gpui::test]
5135async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5136 init_test(cx, |_| {});
5137
5138 let mut cx = EditorTestContext::new(cx).await;
5139
5140 // Manipulate with multiple selections on a single line
5141 cx.set_state(indoc! {"
5142 dd«dd
5143 cˇ»c«c
5144 bb
5145 aaaˇ»aa
5146 "});
5147 cx.update_editor(|e, window, cx| {
5148 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5149 });
5150 cx.assert_editor_state(indoc! {"
5151 «aaaaa
5152 bb
5153 ccc
5154 ddddˇ»
5155 "});
5156
5157 // Manipulate with multiple disjoin selections
5158 cx.set_state(indoc! {"
5159 5«
5160 4
5161 3
5162 2
5163 1ˇ»
5164
5165 dd«dd
5166 ccc
5167 bb
5168 aaaˇ»aa
5169 "});
5170 cx.update_editor(|e, window, cx| {
5171 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5172 });
5173 cx.assert_editor_state(indoc! {"
5174 «1
5175 2
5176 3
5177 4
5178 5ˇ»
5179
5180 «aaaaa
5181 bb
5182 ccc
5183 ddddˇ»
5184 "});
5185
5186 // Adding lines on each selection
5187 cx.set_state(indoc! {"
5188 2«
5189 1ˇ»
5190
5191 bb«bb
5192 aaaˇ»aa
5193 "});
5194 cx.update_editor(|e, window, cx| {
5195 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5196 });
5197 cx.assert_editor_state(indoc! {"
5198 «2
5199 1
5200 added lineˇ»
5201
5202 «bbbb
5203 aaaaa
5204 added lineˇ»
5205 "});
5206
5207 // Removing lines on each selection
5208 cx.set_state(indoc! {"
5209 2«
5210 1ˇ»
5211
5212 bb«bb
5213 aaaˇ»aa
5214 "});
5215 cx.update_editor(|e, window, cx| {
5216 e.manipulate_immutable_lines(window, cx, |lines| {
5217 lines.pop();
5218 })
5219 });
5220 cx.assert_editor_state(indoc! {"
5221 «2ˇ»
5222
5223 «bbbbˇ»
5224 "});
5225}
5226
5227#[gpui::test]
5228async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5229 init_test(cx, |settings| {
5230 settings.defaults.tab_size = NonZeroU32::new(3)
5231 });
5232
5233 let mut cx = EditorTestContext::new(cx).await;
5234
5235 // MULTI SELECTION
5236 // Ln.1 "«" tests empty lines
5237 // Ln.9 tests just leading whitespace
5238 cx.set_state(indoc! {"
5239 «
5240 abc // No indentationˇ»
5241 «\tabc // 1 tabˇ»
5242 \t\tabc « ˇ» // 2 tabs
5243 \t ab«c // Tab followed by space
5244 \tabc // Space followed by tab (3 spaces should be the result)
5245 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5246 abˇ»ˇc ˇ ˇ // Already space indented«
5247 \t
5248 \tabc\tdef // Only the leading tab is manipulatedˇ»
5249 "});
5250 cx.update_editor(|e, window, cx| {
5251 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5252 });
5253 cx.assert_editor_state(
5254 indoc! {"
5255 «
5256 abc // No indentation
5257 abc // 1 tab
5258 abc // 2 tabs
5259 abc // Tab followed by space
5260 abc // Space followed by tab (3 spaces should be the result)
5261 abc // Mixed indentation (tab conversion depends on the column)
5262 abc // Already space indented
5263 ·
5264 abc\tdef // Only the leading tab is manipulatedˇ»
5265 "}
5266 .replace("·", "")
5267 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5268 );
5269
5270 // Test on just a few lines, the others should remain unchanged
5271 // Only lines (3, 5, 10, 11) should change
5272 cx.set_state(
5273 indoc! {"
5274 ·
5275 abc // No indentation
5276 \tabcˇ // 1 tab
5277 \t\tabc // 2 tabs
5278 \t abcˇ // Tab followed by space
5279 \tabc // Space followed by tab (3 spaces should be the result)
5280 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5281 abc // Already space indented
5282 «\t
5283 \tabc\tdef // Only the leading tab is manipulatedˇ»
5284 "}
5285 .replace("·", "")
5286 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5287 );
5288 cx.update_editor(|e, window, cx| {
5289 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5290 });
5291 cx.assert_editor_state(
5292 indoc! {"
5293 ·
5294 abc // No indentation
5295 « abc // 1 tabˇ»
5296 \t\tabc // 2 tabs
5297 « abc // Tab followed by spaceˇ»
5298 \tabc // Space followed by tab (3 spaces should be the result)
5299 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5300 abc // Already space indented
5301 « ·
5302 abc\tdef // Only the leading tab is manipulatedˇ»
5303 "}
5304 .replace("·", "")
5305 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5306 );
5307
5308 // SINGLE SELECTION
5309 // Ln.1 "«" tests empty lines
5310 // Ln.9 tests just leading whitespace
5311 cx.set_state(indoc! {"
5312 «
5313 abc // No indentation
5314 \tabc // 1 tab
5315 \t\tabc // 2 tabs
5316 \t abc // Tab followed by space
5317 \tabc // Space followed by tab (3 spaces should be the result)
5318 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5319 abc // Already space indented
5320 \t
5321 \tabc\tdef // Only the leading tab is manipulatedˇ»
5322 "});
5323 cx.update_editor(|e, window, cx| {
5324 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5325 });
5326 cx.assert_editor_state(
5327 indoc! {"
5328 «
5329 abc // No indentation
5330 abc // 1 tab
5331 abc // 2 tabs
5332 abc // Tab followed by space
5333 abc // Space followed by tab (3 spaces should be the result)
5334 abc // Mixed indentation (tab conversion depends on the column)
5335 abc // Already space indented
5336 ·
5337 abc\tdef // Only the leading tab is manipulatedˇ»
5338 "}
5339 .replace("·", "")
5340 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5341 );
5342}
5343
5344#[gpui::test]
5345async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5346 init_test(cx, |settings| {
5347 settings.defaults.tab_size = NonZeroU32::new(3)
5348 });
5349
5350 let mut cx = EditorTestContext::new(cx).await;
5351
5352 // MULTI SELECTION
5353 // Ln.1 "«" tests empty lines
5354 // Ln.11 tests just leading whitespace
5355 cx.set_state(indoc! {"
5356 «
5357 abˇ»ˇc // No indentation
5358 abc ˇ ˇ // 1 space (< 3 so dont convert)
5359 abc « // 2 spaces (< 3 so dont convert)
5360 abc // 3 spaces (convert)
5361 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5362 «\tˇ»\t«\tˇ»abc // Already tab indented
5363 «\t abc // Tab followed by space
5364 \tabc // Space followed by tab (should be consumed due to tab)
5365 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5366 \tˇ» «\t
5367 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5368 "});
5369 cx.update_editor(|e, window, cx| {
5370 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5371 });
5372 cx.assert_editor_state(indoc! {"
5373 «
5374 abc // No indentation
5375 abc // 1 space (< 3 so dont convert)
5376 abc // 2 spaces (< 3 so dont convert)
5377 \tabc // 3 spaces (convert)
5378 \t abc // 5 spaces (1 tab + 2 spaces)
5379 \t\t\tabc // Already tab indented
5380 \t abc // Tab followed by space
5381 \tabc // Space followed by tab (should be consumed due to tab)
5382 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5383 \t\t\t
5384 \tabc \t // Only the leading spaces should be convertedˇ»
5385 "});
5386
5387 // Test on just a few lines, the other should remain unchanged
5388 // Only lines (4, 8, 11, 12) should change
5389 cx.set_state(
5390 indoc! {"
5391 ·
5392 abc // No indentation
5393 abc // 1 space (< 3 so dont convert)
5394 abc // 2 spaces (< 3 so dont convert)
5395 « abc // 3 spaces (convert)ˇ»
5396 abc // 5 spaces (1 tab + 2 spaces)
5397 \t\t\tabc // Already tab indented
5398 \t abc // Tab followed by space
5399 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5400 \t\t \tabc // Mixed indentation
5401 \t \t \t \tabc // Mixed indentation
5402 \t \tˇ
5403 « abc \t // Only the leading spaces should be convertedˇ»
5404 "}
5405 .replace("·", "")
5406 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5407 );
5408 cx.update_editor(|e, window, cx| {
5409 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5410 });
5411 cx.assert_editor_state(
5412 indoc! {"
5413 ·
5414 abc // No indentation
5415 abc // 1 space (< 3 so dont convert)
5416 abc // 2 spaces (< 3 so dont convert)
5417 «\tabc // 3 spaces (convert)ˇ»
5418 abc // 5 spaces (1 tab + 2 spaces)
5419 \t\t\tabc // Already tab indented
5420 \t abc // Tab followed by space
5421 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5422 \t\t \tabc // Mixed indentation
5423 \t \t \t \tabc // Mixed indentation
5424 «\t\t\t
5425 \tabc \t // Only the leading spaces should be convertedˇ»
5426 "}
5427 .replace("·", "")
5428 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5429 );
5430
5431 // SINGLE SELECTION
5432 // Ln.1 "«" tests empty lines
5433 // Ln.11 tests just leading whitespace
5434 cx.set_state(indoc! {"
5435 «
5436 abc // No indentation
5437 abc // 1 space (< 3 so dont convert)
5438 abc // 2 spaces (< 3 so dont convert)
5439 abc // 3 spaces (convert)
5440 abc // 5 spaces (1 tab + 2 spaces)
5441 \t\t\tabc // Already tab indented
5442 \t abc // Tab followed by space
5443 \tabc // Space followed by tab (should be consumed due to tab)
5444 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5445 \t \t
5446 abc \t // Only the leading spaces should be convertedˇ»
5447 "});
5448 cx.update_editor(|e, window, cx| {
5449 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5450 });
5451 cx.assert_editor_state(indoc! {"
5452 «
5453 abc // No indentation
5454 abc // 1 space (< 3 so dont convert)
5455 abc // 2 spaces (< 3 so dont convert)
5456 \tabc // 3 spaces (convert)
5457 \t abc // 5 spaces (1 tab + 2 spaces)
5458 \t\t\tabc // Already tab indented
5459 \t abc // Tab followed by space
5460 \tabc // Space followed by tab (should be consumed due to tab)
5461 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5462 \t\t\t
5463 \tabc \t // Only the leading spaces should be convertedˇ»
5464 "});
5465}
5466
5467#[gpui::test]
5468async fn test_toggle_case(cx: &mut TestAppContext) {
5469 init_test(cx, |_| {});
5470
5471 let mut cx = EditorTestContext::new(cx).await;
5472
5473 // If all lower case -> upper case
5474 cx.set_state(indoc! {"
5475 «hello worldˇ»
5476 "});
5477 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5478 cx.assert_editor_state(indoc! {"
5479 «HELLO WORLDˇ»
5480 "});
5481
5482 // If all upper case -> lower case
5483 cx.set_state(indoc! {"
5484 «HELLO WORLDˇ»
5485 "});
5486 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5487 cx.assert_editor_state(indoc! {"
5488 «hello worldˇ»
5489 "});
5490
5491 // If any upper case characters are identified -> lower case
5492 // This matches JetBrains IDEs
5493 cx.set_state(indoc! {"
5494 «hEllo worldˇ»
5495 "});
5496 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5497 cx.assert_editor_state(indoc! {"
5498 «hello worldˇ»
5499 "});
5500}
5501
5502#[gpui::test]
5503async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5504 init_test(cx, |_| {});
5505
5506 let mut cx = EditorTestContext::new(cx).await;
5507
5508 cx.set_state(indoc! {"
5509 «implement-windows-supportˇ»
5510 "});
5511 cx.update_editor(|e, window, cx| {
5512 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5513 });
5514 cx.assert_editor_state(indoc! {"
5515 «Implement windows supportˇ»
5516 "});
5517}
5518
5519#[gpui::test]
5520async fn test_manipulate_text(cx: &mut TestAppContext) {
5521 init_test(cx, |_| {});
5522
5523 let mut cx = EditorTestContext::new(cx).await;
5524
5525 // Test convert_to_upper_case()
5526 cx.set_state(indoc! {"
5527 «hello worldˇ»
5528 "});
5529 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5530 cx.assert_editor_state(indoc! {"
5531 «HELLO WORLDˇ»
5532 "});
5533
5534 // Test convert_to_lower_case()
5535 cx.set_state(indoc! {"
5536 «HELLO WORLDˇ»
5537 "});
5538 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5539 cx.assert_editor_state(indoc! {"
5540 «hello worldˇ»
5541 "});
5542
5543 // Test multiple line, single selection case
5544 cx.set_state(indoc! {"
5545 «The quick brown
5546 fox jumps over
5547 the lazy dogˇ»
5548 "});
5549 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5550 cx.assert_editor_state(indoc! {"
5551 «The Quick Brown
5552 Fox Jumps Over
5553 The Lazy Dogˇ»
5554 "});
5555
5556 // Test multiple line, single selection case
5557 cx.set_state(indoc! {"
5558 «The quick brown
5559 fox jumps over
5560 the lazy dogˇ»
5561 "});
5562 cx.update_editor(|e, window, cx| {
5563 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5564 });
5565 cx.assert_editor_state(indoc! {"
5566 «TheQuickBrown
5567 FoxJumpsOver
5568 TheLazyDogˇ»
5569 "});
5570
5571 // From here on out, test more complex cases of manipulate_text()
5572
5573 // Test no selection case - should affect words cursors are in
5574 // Cursor at beginning, middle, and end of word
5575 cx.set_state(indoc! {"
5576 ˇhello big beauˇtiful worldˇ
5577 "});
5578 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5579 cx.assert_editor_state(indoc! {"
5580 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5581 "});
5582
5583 // Test multiple selections on a single line and across multiple lines
5584 cx.set_state(indoc! {"
5585 «Theˇ» quick «brown
5586 foxˇ» jumps «overˇ»
5587 the «lazyˇ» dog
5588 "});
5589 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5590 cx.assert_editor_state(indoc! {"
5591 «THEˇ» quick «BROWN
5592 FOXˇ» jumps «OVERˇ»
5593 the «LAZYˇ» dog
5594 "});
5595
5596 // Test case where text length grows
5597 cx.set_state(indoc! {"
5598 «tschüߡ»
5599 "});
5600 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5601 cx.assert_editor_state(indoc! {"
5602 «TSCHÜSSˇ»
5603 "});
5604
5605 // Test to make sure we don't crash when text shrinks
5606 cx.set_state(indoc! {"
5607 aaa_bbbˇ
5608 "});
5609 cx.update_editor(|e, window, cx| {
5610 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5611 });
5612 cx.assert_editor_state(indoc! {"
5613 «aaaBbbˇ»
5614 "});
5615
5616 // Test to make sure we all aware of the fact that each word can grow and shrink
5617 // Final selections should be aware of this fact
5618 cx.set_state(indoc! {"
5619 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5620 "});
5621 cx.update_editor(|e, window, cx| {
5622 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5623 });
5624 cx.assert_editor_state(indoc! {"
5625 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5626 "});
5627
5628 cx.set_state(indoc! {"
5629 «hElLo, WoRld!ˇ»
5630 "});
5631 cx.update_editor(|e, window, cx| {
5632 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5633 });
5634 cx.assert_editor_state(indoc! {"
5635 «HeLlO, wOrLD!ˇ»
5636 "});
5637
5638 // Test selections with `line_mode() = true`.
5639 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5640 cx.set_state(indoc! {"
5641 «The quick brown
5642 fox jumps over
5643 tˇ»he lazy dog
5644 "});
5645 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5646 cx.assert_editor_state(indoc! {"
5647 «THE QUICK BROWN
5648 FOX JUMPS OVER
5649 THE LAZY DOGˇ»
5650 "});
5651}
5652
5653#[gpui::test]
5654fn test_duplicate_line(cx: &mut TestAppContext) {
5655 init_test(cx, |_| {});
5656
5657 let editor = cx.add_window(|window, cx| {
5658 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5659 build_editor(buffer, window, cx)
5660 });
5661 _ = editor.update(cx, |editor, window, cx| {
5662 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5663 s.select_display_ranges([
5664 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5665 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5666 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5667 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5668 ])
5669 });
5670 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5671 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5672 assert_eq!(
5673 display_ranges(editor, cx),
5674 vec![
5675 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5676 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5677 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5678 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5679 ]
5680 );
5681 });
5682
5683 let editor = cx.add_window(|window, cx| {
5684 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5685 build_editor(buffer, window, cx)
5686 });
5687 _ = editor.update(cx, |editor, window, cx| {
5688 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5689 s.select_display_ranges([
5690 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5691 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5692 ])
5693 });
5694 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5695 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5696 assert_eq!(
5697 display_ranges(editor, cx),
5698 vec![
5699 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5700 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5701 ]
5702 );
5703 });
5704
5705 // With `duplicate_line_up` the selections move to the duplicated lines,
5706 // which are inserted above the original lines
5707 let editor = cx.add_window(|window, cx| {
5708 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5709 build_editor(buffer, window, cx)
5710 });
5711 _ = editor.update(cx, |editor, window, cx| {
5712 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5713 s.select_display_ranges([
5714 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5715 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5716 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5717 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5718 ])
5719 });
5720 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5721 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5722 assert_eq!(
5723 display_ranges(editor, cx),
5724 vec![
5725 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5726 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5727 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5728 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0),
5729 ]
5730 );
5731 });
5732
5733 let editor = cx.add_window(|window, cx| {
5734 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5735 build_editor(buffer, window, cx)
5736 });
5737 _ = editor.update(cx, |editor, window, cx| {
5738 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5739 s.select_display_ranges([
5740 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5741 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5742 ])
5743 });
5744 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5745 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5746 assert_eq!(
5747 display_ranges(editor, cx),
5748 vec![
5749 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5750 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5751 ]
5752 );
5753 });
5754
5755 let editor = cx.add_window(|window, cx| {
5756 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5757 build_editor(buffer, window, cx)
5758 });
5759 _ = editor.update(cx, |editor, window, cx| {
5760 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5761 s.select_display_ranges([
5762 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5763 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5764 ])
5765 });
5766 editor.duplicate_selection(&DuplicateSelection, window, cx);
5767 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5768 assert_eq!(
5769 display_ranges(editor, cx),
5770 vec![
5771 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5772 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5773 ]
5774 );
5775 });
5776}
5777
5778#[gpui::test]
5779async fn test_rotate_selections(cx: &mut TestAppContext) {
5780 init_test(cx, |_| {});
5781
5782 let mut cx = EditorTestContext::new(cx).await;
5783
5784 // Rotate text selections (horizontal)
5785 cx.set_state("x=«1ˇ», y=«2ˇ», z=«3ˇ»");
5786 cx.update_editor(|e, window, cx| {
5787 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5788 });
5789 cx.assert_editor_state("x=«3ˇ», y=«1ˇ», z=«2ˇ»");
5790 cx.update_editor(|e, window, cx| {
5791 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5792 });
5793 cx.assert_editor_state("x=«1ˇ», y=«2ˇ», z=«3ˇ»");
5794
5795 // Rotate text selections (vertical)
5796 cx.set_state(indoc! {"
5797 x=«1ˇ»
5798 y=«2ˇ»
5799 z=«3ˇ»
5800 "});
5801 cx.update_editor(|e, window, cx| {
5802 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5803 });
5804 cx.assert_editor_state(indoc! {"
5805 x=«3ˇ»
5806 y=«1ˇ»
5807 z=«2ˇ»
5808 "});
5809 cx.update_editor(|e, window, cx| {
5810 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5811 });
5812 cx.assert_editor_state(indoc! {"
5813 x=«1ˇ»
5814 y=«2ˇ»
5815 z=«3ˇ»
5816 "});
5817
5818 // Rotate text selections (vertical, different lengths)
5819 cx.set_state(indoc! {"
5820 x=\"«ˇ»\"
5821 y=\"«aˇ»\"
5822 z=\"«aaˇ»\"
5823 "});
5824 cx.update_editor(|e, window, cx| {
5825 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5826 });
5827 cx.assert_editor_state(indoc! {"
5828 x=\"«aaˇ»\"
5829 y=\"«ˇ»\"
5830 z=\"«aˇ»\"
5831 "});
5832 cx.update_editor(|e, window, cx| {
5833 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5834 });
5835 cx.assert_editor_state(indoc! {"
5836 x=\"«ˇ»\"
5837 y=\"«aˇ»\"
5838 z=\"«aaˇ»\"
5839 "});
5840
5841 // Rotate whole lines (cursor positions preserved)
5842 cx.set_state(indoc! {"
5843 ˇline123
5844 liˇne23
5845 line3ˇ
5846 "});
5847 cx.update_editor(|e, window, cx| {
5848 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5849 });
5850 cx.assert_editor_state(indoc! {"
5851 line3ˇ
5852 ˇline123
5853 liˇne23
5854 "});
5855 cx.update_editor(|e, window, cx| {
5856 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5857 });
5858 cx.assert_editor_state(indoc! {"
5859 ˇline123
5860 liˇne23
5861 line3ˇ
5862 "});
5863
5864 // Rotate whole lines, multiple cursors per line (positions preserved)
5865 cx.set_state(indoc! {"
5866 ˇliˇne123
5867 ˇline23
5868 ˇline3
5869 "});
5870 cx.update_editor(|e, window, cx| {
5871 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5872 });
5873 cx.assert_editor_state(indoc! {"
5874 ˇline3
5875 ˇliˇne123
5876 ˇline23
5877 "});
5878 cx.update_editor(|e, window, cx| {
5879 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5880 });
5881 cx.assert_editor_state(indoc! {"
5882 ˇliˇne123
5883 ˇline23
5884 ˇline3
5885 "});
5886}
5887
5888#[gpui::test]
5889fn test_move_line_up_down(cx: &mut TestAppContext) {
5890 init_test(cx, |_| {});
5891
5892 let editor = cx.add_window(|window, cx| {
5893 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5894 build_editor(buffer, window, cx)
5895 });
5896 _ = editor.update(cx, |editor, window, cx| {
5897 editor.fold_creases(
5898 vec![
5899 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5900 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5901 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5902 ],
5903 true,
5904 window,
5905 cx,
5906 );
5907 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5908 s.select_display_ranges([
5909 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5910 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5911 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5912 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5913 ])
5914 });
5915 assert_eq!(
5916 editor.display_text(cx),
5917 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5918 );
5919
5920 editor.move_line_up(&MoveLineUp, window, cx);
5921 assert_eq!(
5922 editor.display_text(cx),
5923 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5924 );
5925 assert_eq!(
5926 display_ranges(editor, cx),
5927 vec![
5928 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5929 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5930 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5931 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5932 ]
5933 );
5934 });
5935
5936 _ = editor.update(cx, |editor, window, cx| {
5937 editor.move_line_down(&MoveLineDown, window, cx);
5938 assert_eq!(
5939 editor.display_text(cx),
5940 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5941 );
5942 assert_eq!(
5943 display_ranges(editor, cx),
5944 vec![
5945 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5946 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5947 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5948 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5949 ]
5950 );
5951 });
5952
5953 _ = editor.update(cx, |editor, window, cx| {
5954 editor.move_line_down(&MoveLineDown, window, cx);
5955 assert_eq!(
5956 editor.display_text(cx),
5957 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5958 );
5959 assert_eq!(
5960 display_ranges(editor, cx),
5961 vec![
5962 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5963 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5964 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5965 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5966 ]
5967 );
5968 });
5969
5970 _ = editor.update(cx, |editor, window, cx| {
5971 editor.move_line_up(&MoveLineUp, window, cx);
5972 assert_eq!(
5973 editor.display_text(cx),
5974 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5975 );
5976 assert_eq!(
5977 display_ranges(editor, cx),
5978 vec![
5979 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5980 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5981 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5982 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5983 ]
5984 );
5985 });
5986}
5987
5988#[gpui::test]
5989fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5990 init_test(cx, |_| {});
5991 let editor = cx.add_window(|window, cx| {
5992 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5993 build_editor(buffer, window, cx)
5994 });
5995 _ = editor.update(cx, |editor, window, cx| {
5996 editor.fold_creases(
5997 vec![Crease::simple(
5998 Point::new(6, 4)..Point::new(7, 4),
5999 FoldPlaceholder::test(),
6000 )],
6001 true,
6002 window,
6003 cx,
6004 );
6005 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6006 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
6007 });
6008 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
6009 editor.move_line_up(&MoveLineUp, window, cx);
6010 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
6011 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
6012 });
6013}
6014
6015#[gpui::test]
6016fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
6017 init_test(cx, |_| {});
6018
6019 let editor = cx.add_window(|window, cx| {
6020 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
6021 build_editor(buffer, window, cx)
6022 });
6023 _ = editor.update(cx, |editor, window, cx| {
6024 let snapshot = editor.buffer.read(cx).snapshot(cx);
6025 editor.insert_blocks(
6026 [BlockProperties {
6027 style: BlockStyle::Fixed,
6028 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
6029 height: Some(1),
6030 render: Arc::new(|_| div().into_any()),
6031 priority: 0,
6032 }],
6033 Some(Autoscroll::fit()),
6034 cx,
6035 );
6036 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6037 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
6038 });
6039 editor.move_line_down(&MoveLineDown, window, cx);
6040 });
6041}
6042
6043#[gpui::test]
6044async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
6045 init_test(cx, |_| {});
6046
6047 let mut cx = EditorTestContext::new(cx).await;
6048 cx.set_state(
6049 &"
6050 ˇzero
6051 one
6052 two
6053 three
6054 four
6055 five
6056 "
6057 .unindent(),
6058 );
6059
6060 // Create a four-line block that replaces three lines of text.
6061 cx.update_editor(|editor, window, cx| {
6062 let snapshot = editor.snapshot(window, cx);
6063 let snapshot = &snapshot.buffer_snapshot();
6064 let placement = BlockPlacement::Replace(
6065 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
6066 );
6067 editor.insert_blocks(
6068 [BlockProperties {
6069 placement,
6070 height: Some(4),
6071 style: BlockStyle::Sticky,
6072 render: Arc::new(|_| gpui::div().into_any_element()),
6073 priority: 0,
6074 }],
6075 None,
6076 cx,
6077 );
6078 });
6079
6080 // Move down so that the cursor touches the block.
6081 cx.update_editor(|editor, window, cx| {
6082 editor.move_down(&Default::default(), window, cx);
6083 });
6084 cx.assert_editor_state(
6085 &"
6086 zero
6087 «one
6088 two
6089 threeˇ»
6090 four
6091 five
6092 "
6093 .unindent(),
6094 );
6095
6096 // Move down past the block.
6097 cx.update_editor(|editor, window, cx| {
6098 editor.move_down(&Default::default(), window, cx);
6099 });
6100 cx.assert_editor_state(
6101 &"
6102 zero
6103 one
6104 two
6105 three
6106 ˇfour
6107 five
6108 "
6109 .unindent(),
6110 );
6111}
6112
6113#[gpui::test]
6114fn test_transpose(cx: &mut TestAppContext) {
6115 init_test(cx, |_| {});
6116
6117 _ = cx.add_window(|window, cx| {
6118 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
6119 editor.set_style(EditorStyle::default(), window, cx);
6120 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6121 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
6122 });
6123 editor.transpose(&Default::default(), window, cx);
6124 assert_eq!(editor.text(cx), "bac");
6125 assert_eq!(
6126 editor.selections.ranges(&editor.display_snapshot(cx)),
6127 [MultiBufferOffset(2)..MultiBufferOffset(2)]
6128 );
6129
6130 editor.transpose(&Default::default(), window, cx);
6131 assert_eq!(editor.text(cx), "bca");
6132 assert_eq!(
6133 editor.selections.ranges(&editor.display_snapshot(cx)),
6134 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6135 );
6136
6137 editor.transpose(&Default::default(), window, cx);
6138 assert_eq!(editor.text(cx), "bac");
6139 assert_eq!(
6140 editor.selections.ranges(&editor.display_snapshot(cx)),
6141 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6142 );
6143
6144 editor
6145 });
6146
6147 _ = cx.add_window(|window, cx| {
6148 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6149 editor.set_style(EditorStyle::default(), window, cx);
6150 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6151 s.select_ranges([MultiBufferOffset(3)..MultiBufferOffset(3)])
6152 });
6153 editor.transpose(&Default::default(), window, cx);
6154 assert_eq!(editor.text(cx), "acb\nde");
6155 assert_eq!(
6156 editor.selections.ranges(&editor.display_snapshot(cx)),
6157 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6158 );
6159
6160 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6161 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6162 });
6163 editor.transpose(&Default::default(), window, cx);
6164 assert_eq!(editor.text(cx), "acbd\ne");
6165 assert_eq!(
6166 editor.selections.ranges(&editor.display_snapshot(cx)),
6167 [MultiBufferOffset(5)..MultiBufferOffset(5)]
6168 );
6169
6170 editor.transpose(&Default::default(), window, cx);
6171 assert_eq!(editor.text(cx), "acbde\n");
6172 assert_eq!(
6173 editor.selections.ranges(&editor.display_snapshot(cx)),
6174 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6175 );
6176
6177 editor.transpose(&Default::default(), window, cx);
6178 assert_eq!(editor.text(cx), "acbd\ne");
6179 assert_eq!(
6180 editor.selections.ranges(&editor.display_snapshot(cx)),
6181 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6182 );
6183
6184 editor
6185 });
6186
6187 _ = cx.add_window(|window, cx| {
6188 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6189 editor.set_style(EditorStyle::default(), window, cx);
6190 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6191 s.select_ranges([
6192 MultiBufferOffset(1)..MultiBufferOffset(1),
6193 MultiBufferOffset(2)..MultiBufferOffset(2),
6194 MultiBufferOffset(4)..MultiBufferOffset(4),
6195 ])
6196 });
6197 editor.transpose(&Default::default(), window, cx);
6198 assert_eq!(editor.text(cx), "bacd\ne");
6199 assert_eq!(
6200 editor.selections.ranges(&editor.display_snapshot(cx)),
6201 [
6202 MultiBufferOffset(2)..MultiBufferOffset(2),
6203 MultiBufferOffset(3)..MultiBufferOffset(3),
6204 MultiBufferOffset(5)..MultiBufferOffset(5)
6205 ]
6206 );
6207
6208 editor.transpose(&Default::default(), window, cx);
6209 assert_eq!(editor.text(cx), "bcade\n");
6210 assert_eq!(
6211 editor.selections.ranges(&editor.display_snapshot(cx)),
6212 [
6213 MultiBufferOffset(3)..MultiBufferOffset(3),
6214 MultiBufferOffset(4)..MultiBufferOffset(4),
6215 MultiBufferOffset(6)..MultiBufferOffset(6)
6216 ]
6217 );
6218
6219 editor.transpose(&Default::default(), window, cx);
6220 assert_eq!(editor.text(cx), "bcda\ne");
6221 assert_eq!(
6222 editor.selections.ranges(&editor.display_snapshot(cx)),
6223 [
6224 MultiBufferOffset(4)..MultiBufferOffset(4),
6225 MultiBufferOffset(6)..MultiBufferOffset(6)
6226 ]
6227 );
6228
6229 editor.transpose(&Default::default(), window, cx);
6230 assert_eq!(editor.text(cx), "bcade\n");
6231 assert_eq!(
6232 editor.selections.ranges(&editor.display_snapshot(cx)),
6233 [
6234 MultiBufferOffset(4)..MultiBufferOffset(4),
6235 MultiBufferOffset(6)..MultiBufferOffset(6)
6236 ]
6237 );
6238
6239 editor.transpose(&Default::default(), window, cx);
6240 assert_eq!(editor.text(cx), "bcaed\n");
6241 assert_eq!(
6242 editor.selections.ranges(&editor.display_snapshot(cx)),
6243 [
6244 MultiBufferOffset(5)..MultiBufferOffset(5),
6245 MultiBufferOffset(6)..MultiBufferOffset(6)
6246 ]
6247 );
6248
6249 editor
6250 });
6251
6252 _ = cx.add_window(|window, cx| {
6253 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6254 editor.set_style(EditorStyle::default(), window, cx);
6255 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6256 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6257 });
6258 editor.transpose(&Default::default(), window, cx);
6259 assert_eq!(editor.text(cx), "🏀🍐✋");
6260 assert_eq!(
6261 editor.selections.ranges(&editor.display_snapshot(cx)),
6262 [MultiBufferOffset(8)..MultiBufferOffset(8)]
6263 );
6264
6265 editor.transpose(&Default::default(), window, cx);
6266 assert_eq!(editor.text(cx), "🏀✋🍐");
6267 assert_eq!(
6268 editor.selections.ranges(&editor.display_snapshot(cx)),
6269 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6270 );
6271
6272 editor.transpose(&Default::default(), window, cx);
6273 assert_eq!(editor.text(cx), "🏀🍐✋");
6274 assert_eq!(
6275 editor.selections.ranges(&editor.display_snapshot(cx)),
6276 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6277 );
6278
6279 editor
6280 });
6281}
6282
6283#[gpui::test]
6284async fn test_rewrap(cx: &mut TestAppContext) {
6285 init_test(cx, |settings| {
6286 settings.languages.0.extend([
6287 (
6288 "Markdown".into(),
6289 LanguageSettingsContent {
6290 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6291 preferred_line_length: Some(40),
6292 ..Default::default()
6293 },
6294 ),
6295 (
6296 "Plain Text".into(),
6297 LanguageSettingsContent {
6298 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6299 preferred_line_length: Some(40),
6300 ..Default::default()
6301 },
6302 ),
6303 (
6304 "C++".into(),
6305 LanguageSettingsContent {
6306 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6307 preferred_line_length: Some(40),
6308 ..Default::default()
6309 },
6310 ),
6311 (
6312 "Python".into(),
6313 LanguageSettingsContent {
6314 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6315 preferred_line_length: Some(40),
6316 ..Default::default()
6317 },
6318 ),
6319 (
6320 "Rust".into(),
6321 LanguageSettingsContent {
6322 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6323 preferred_line_length: Some(40),
6324 ..Default::default()
6325 },
6326 ),
6327 ])
6328 });
6329
6330 let mut cx = EditorTestContext::new(cx).await;
6331
6332 let cpp_language = Arc::new(Language::new(
6333 LanguageConfig {
6334 name: "C++".into(),
6335 line_comments: vec!["// ".into()],
6336 ..LanguageConfig::default()
6337 },
6338 None,
6339 ));
6340 let python_language = Arc::new(Language::new(
6341 LanguageConfig {
6342 name: "Python".into(),
6343 line_comments: vec!["# ".into()],
6344 ..LanguageConfig::default()
6345 },
6346 None,
6347 ));
6348 let markdown_language = Arc::new(Language::new(
6349 LanguageConfig {
6350 name: "Markdown".into(),
6351 rewrap_prefixes: vec![
6352 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6353 regex::Regex::new("[-*+]\\s+").unwrap(),
6354 ],
6355 ..LanguageConfig::default()
6356 },
6357 None,
6358 ));
6359 let rust_language = Arc::new(
6360 Language::new(
6361 LanguageConfig {
6362 name: "Rust".into(),
6363 line_comments: vec!["// ".into(), "/// ".into()],
6364 ..LanguageConfig::default()
6365 },
6366 Some(tree_sitter_rust::LANGUAGE.into()),
6367 )
6368 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6369 .unwrap(),
6370 );
6371
6372 let plaintext_language = Arc::new(Language::new(
6373 LanguageConfig {
6374 name: "Plain Text".into(),
6375 ..LanguageConfig::default()
6376 },
6377 None,
6378 ));
6379
6380 // Test basic rewrapping of a long line with a cursor
6381 assert_rewrap(
6382 indoc! {"
6383 // ˇThis is a long comment that needs to be wrapped.
6384 "},
6385 indoc! {"
6386 // ˇThis is a long comment that needs to
6387 // be wrapped.
6388 "},
6389 cpp_language.clone(),
6390 &mut cx,
6391 );
6392
6393 // Test rewrapping a full selection
6394 assert_rewrap(
6395 indoc! {"
6396 «// This selected long comment needs to be wrapped.ˇ»"
6397 },
6398 indoc! {"
6399 «// This selected long comment needs to
6400 // be wrapped.ˇ»"
6401 },
6402 cpp_language.clone(),
6403 &mut cx,
6404 );
6405
6406 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6407 assert_rewrap(
6408 indoc! {"
6409 // ˇThis is the first line.
6410 // Thisˇ is the second line.
6411 // This is the thirdˇ line, all part of one paragraph.
6412 "},
6413 indoc! {"
6414 // ˇThis is the first line. Thisˇ is the
6415 // second line. This is the thirdˇ line,
6416 // all part of one paragraph.
6417 "},
6418 cpp_language.clone(),
6419 &mut cx,
6420 );
6421
6422 // Test multiple cursors in different paragraphs trigger separate rewraps
6423 assert_rewrap(
6424 indoc! {"
6425 // ˇThis is the first paragraph, first line.
6426 // ˇThis is the first paragraph, second line.
6427
6428 // ˇThis is the second paragraph, first line.
6429 // ˇThis is the second paragraph, second line.
6430 "},
6431 indoc! {"
6432 // ˇThis is the first paragraph, first
6433 // line. ˇThis is the first paragraph,
6434 // second line.
6435
6436 // ˇThis is the second paragraph, first
6437 // line. ˇThis is the second paragraph,
6438 // second line.
6439 "},
6440 cpp_language.clone(),
6441 &mut cx,
6442 );
6443
6444 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6445 assert_rewrap(
6446 indoc! {"
6447 «// A regular long long comment to be wrapped.
6448 /// A documentation long comment to be wrapped.ˇ»
6449 "},
6450 indoc! {"
6451 «// A regular long long comment to be
6452 // wrapped.
6453 /// A documentation long comment to be
6454 /// wrapped.ˇ»
6455 "},
6456 rust_language.clone(),
6457 &mut cx,
6458 );
6459
6460 // Test that change in indentation level trigger seperate rewraps
6461 assert_rewrap(
6462 indoc! {"
6463 fn foo() {
6464 «// This is a long comment at the base indent.
6465 // This is a long comment at the next indent.ˇ»
6466 }
6467 "},
6468 indoc! {"
6469 fn foo() {
6470 «// This is a long comment at the
6471 // base indent.
6472 // This is a long comment at the
6473 // next indent.ˇ»
6474 }
6475 "},
6476 rust_language.clone(),
6477 &mut cx,
6478 );
6479
6480 // Test that different comment prefix characters (e.g., '#') are handled correctly
6481 assert_rewrap(
6482 indoc! {"
6483 # ˇThis is a long comment using a pound sign.
6484 "},
6485 indoc! {"
6486 # ˇThis is a long comment using a pound
6487 # sign.
6488 "},
6489 python_language,
6490 &mut cx,
6491 );
6492
6493 // Test rewrapping only affects comments, not code even when selected
6494 assert_rewrap(
6495 indoc! {"
6496 «/// This doc comment is long and should be wrapped.
6497 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6498 "},
6499 indoc! {"
6500 «/// This doc comment is long and should
6501 /// be wrapped.
6502 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6503 "},
6504 rust_language.clone(),
6505 &mut cx,
6506 );
6507
6508 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6509 assert_rewrap(
6510 indoc! {"
6511 # Header
6512
6513 A long long long line of markdown text to wrap.ˇ
6514 "},
6515 indoc! {"
6516 # Header
6517
6518 A long long long line of markdown text
6519 to wrap.ˇ
6520 "},
6521 markdown_language.clone(),
6522 &mut cx,
6523 );
6524
6525 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6526 assert_rewrap(
6527 indoc! {"
6528 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6529 2. This is a numbered list item that is very long and needs to be wrapped properly.
6530 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6531 "},
6532 indoc! {"
6533 «1. This is a numbered list item that is
6534 very long and needs to be wrapped
6535 properly.
6536 2. This is a numbered list item that is
6537 very long and needs to be wrapped
6538 properly.
6539 - This is an unordered list item that is
6540 also very long and should not merge
6541 with the numbered item.ˇ»
6542 "},
6543 markdown_language.clone(),
6544 &mut cx,
6545 );
6546
6547 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6548 assert_rewrap(
6549 indoc! {"
6550 «1. This is a numbered list item that is
6551 very long and needs to be wrapped
6552 properly.
6553 2. This is a numbered list item that is
6554 very long and needs to be wrapped
6555 properly.
6556 - This is an unordered list item that is
6557 also very long and should not merge with
6558 the numbered item.ˇ»
6559 "},
6560 indoc! {"
6561 «1. This is a numbered list item that is
6562 very long and needs to be wrapped
6563 properly.
6564 2. This is a numbered list item that is
6565 very long and needs to be wrapped
6566 properly.
6567 - This is an unordered list item that is
6568 also very long and should not merge
6569 with the numbered item.ˇ»
6570 "},
6571 markdown_language.clone(),
6572 &mut cx,
6573 );
6574
6575 // Test that rewrapping maintain indents even when they already exists.
6576 assert_rewrap(
6577 indoc! {"
6578 «1. This is a numbered list
6579 item that is very long and needs to be wrapped properly.
6580 2. This is a numbered list
6581 item that is very long and needs to be wrapped properly.
6582 - This is an unordered list item that is also very long and
6583 should not merge with the numbered item.ˇ»
6584 "},
6585 indoc! {"
6586 «1. This is a numbered list item that is
6587 very long and needs to be wrapped
6588 properly.
6589 2. This is a numbered list item that is
6590 very long and needs to be wrapped
6591 properly.
6592 - This is an unordered list item that is
6593 also very long and should not merge
6594 with the numbered item.ˇ»
6595 "},
6596 markdown_language,
6597 &mut cx,
6598 );
6599
6600 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6601 assert_rewrap(
6602 indoc! {"
6603 ˇThis is a very long line of plain text that will be wrapped.
6604 "},
6605 indoc! {"
6606 ˇThis is a very long line of plain text
6607 that will be wrapped.
6608 "},
6609 plaintext_language.clone(),
6610 &mut cx,
6611 );
6612
6613 // Test that non-commented code acts as a paragraph boundary within a selection
6614 assert_rewrap(
6615 indoc! {"
6616 «// This is the first long comment block to be wrapped.
6617 fn my_func(a: u32);
6618 // This is the second long comment block to be wrapped.ˇ»
6619 "},
6620 indoc! {"
6621 «// This is the first long comment block
6622 // to be wrapped.
6623 fn my_func(a: u32);
6624 // This is the second long comment block
6625 // to be wrapped.ˇ»
6626 "},
6627 rust_language,
6628 &mut cx,
6629 );
6630
6631 // Test rewrapping multiple selections, including ones with blank lines or tabs
6632 assert_rewrap(
6633 indoc! {"
6634 «ˇThis is a very long line that will be wrapped.
6635
6636 This is another paragraph in the same selection.»
6637
6638 «\tThis is a very long indented line that will be wrapped.ˇ»
6639 "},
6640 indoc! {"
6641 «ˇThis is a very long line that will be
6642 wrapped.
6643
6644 This is another paragraph in the same
6645 selection.»
6646
6647 «\tThis is a very long indented line
6648 \tthat will be wrapped.ˇ»
6649 "},
6650 plaintext_language,
6651 &mut cx,
6652 );
6653
6654 // Test that an empty comment line acts as a paragraph boundary
6655 assert_rewrap(
6656 indoc! {"
6657 // ˇThis is a long comment that will be wrapped.
6658 //
6659 // And this is another long comment that will also be wrapped.ˇ
6660 "},
6661 indoc! {"
6662 // ˇThis is a long comment that will be
6663 // wrapped.
6664 //
6665 // And this is another long comment that
6666 // will also be wrapped.ˇ
6667 "},
6668 cpp_language,
6669 &mut cx,
6670 );
6671
6672 #[track_caller]
6673 fn assert_rewrap(
6674 unwrapped_text: &str,
6675 wrapped_text: &str,
6676 language: Arc<Language>,
6677 cx: &mut EditorTestContext,
6678 ) {
6679 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6680 cx.set_state(unwrapped_text);
6681 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6682 cx.assert_editor_state(wrapped_text);
6683 }
6684}
6685
6686#[gpui::test]
6687async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6688 init_test(cx, |settings| {
6689 settings.languages.0.extend([(
6690 "Rust".into(),
6691 LanguageSettingsContent {
6692 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6693 preferred_line_length: Some(40),
6694 ..Default::default()
6695 },
6696 )])
6697 });
6698
6699 let mut cx = EditorTestContext::new(cx).await;
6700
6701 let rust_lang = Arc::new(
6702 Language::new(
6703 LanguageConfig {
6704 name: "Rust".into(),
6705 line_comments: vec!["// ".into()],
6706 block_comment: Some(BlockCommentConfig {
6707 start: "/*".into(),
6708 end: "*/".into(),
6709 prefix: "* ".into(),
6710 tab_size: 1,
6711 }),
6712 documentation_comment: Some(BlockCommentConfig {
6713 start: "/**".into(),
6714 end: "*/".into(),
6715 prefix: "* ".into(),
6716 tab_size: 1,
6717 }),
6718
6719 ..LanguageConfig::default()
6720 },
6721 Some(tree_sitter_rust::LANGUAGE.into()),
6722 )
6723 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6724 .unwrap(),
6725 );
6726
6727 // regular block comment
6728 assert_rewrap(
6729 indoc! {"
6730 /*
6731 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6732 */
6733 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6734 "},
6735 indoc! {"
6736 /*
6737 *ˇ Lorem ipsum dolor sit amet,
6738 * consectetur adipiscing elit.
6739 */
6740 /*
6741 *ˇ Lorem ipsum dolor sit amet,
6742 * consectetur adipiscing elit.
6743 */
6744 "},
6745 rust_lang.clone(),
6746 &mut cx,
6747 );
6748
6749 // indent is respected
6750 assert_rewrap(
6751 indoc! {"
6752 {}
6753 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6754 "},
6755 indoc! {"
6756 {}
6757 /*
6758 *ˇ Lorem ipsum dolor sit amet,
6759 * consectetur adipiscing elit.
6760 */
6761 "},
6762 rust_lang.clone(),
6763 &mut cx,
6764 );
6765
6766 // short block comments with inline delimiters
6767 assert_rewrap(
6768 indoc! {"
6769 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6770 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6771 */
6772 /*
6773 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6774 "},
6775 indoc! {"
6776 /*
6777 *ˇ Lorem ipsum dolor sit amet,
6778 * consectetur adipiscing elit.
6779 */
6780 /*
6781 *ˇ Lorem ipsum dolor sit amet,
6782 * consectetur adipiscing elit.
6783 */
6784 /*
6785 *ˇ Lorem ipsum dolor sit amet,
6786 * consectetur adipiscing elit.
6787 */
6788 "},
6789 rust_lang.clone(),
6790 &mut cx,
6791 );
6792
6793 // multiline block comment with inline start/end delimiters
6794 assert_rewrap(
6795 indoc! {"
6796 /*ˇ Lorem ipsum dolor sit amet,
6797 * consectetur adipiscing elit. */
6798 "},
6799 indoc! {"
6800 /*
6801 *ˇ Lorem ipsum dolor sit amet,
6802 * consectetur adipiscing elit.
6803 */
6804 "},
6805 rust_lang.clone(),
6806 &mut cx,
6807 );
6808
6809 // block comment rewrap still respects paragraph bounds
6810 assert_rewrap(
6811 indoc! {"
6812 /*
6813 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6814 *
6815 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6816 */
6817 "},
6818 indoc! {"
6819 /*
6820 *ˇ Lorem ipsum dolor sit amet,
6821 * consectetur adipiscing elit.
6822 *
6823 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6824 */
6825 "},
6826 rust_lang.clone(),
6827 &mut cx,
6828 );
6829
6830 // documentation comments
6831 assert_rewrap(
6832 indoc! {"
6833 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6834 /**
6835 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6836 */
6837 "},
6838 indoc! {"
6839 /**
6840 *ˇ Lorem ipsum dolor sit amet,
6841 * consectetur adipiscing elit.
6842 */
6843 /**
6844 *ˇ Lorem ipsum dolor sit amet,
6845 * consectetur adipiscing elit.
6846 */
6847 "},
6848 rust_lang.clone(),
6849 &mut cx,
6850 );
6851
6852 // different, adjacent comments
6853 assert_rewrap(
6854 indoc! {"
6855 /**
6856 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6857 */
6858 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6859 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6860 "},
6861 indoc! {"
6862 /**
6863 *ˇ Lorem ipsum dolor sit amet,
6864 * consectetur adipiscing elit.
6865 */
6866 /*
6867 *ˇ Lorem ipsum dolor sit amet,
6868 * consectetur adipiscing elit.
6869 */
6870 //ˇ Lorem ipsum dolor sit amet,
6871 // consectetur adipiscing elit.
6872 "},
6873 rust_lang.clone(),
6874 &mut cx,
6875 );
6876
6877 // selection w/ single short block comment
6878 assert_rewrap(
6879 indoc! {"
6880 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6881 "},
6882 indoc! {"
6883 «/*
6884 * Lorem ipsum dolor sit amet,
6885 * consectetur adipiscing elit.
6886 */ˇ»
6887 "},
6888 rust_lang.clone(),
6889 &mut cx,
6890 );
6891
6892 // rewrapping a single comment w/ abutting comments
6893 assert_rewrap(
6894 indoc! {"
6895 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6896 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6897 "},
6898 indoc! {"
6899 /*
6900 * ˇLorem ipsum dolor sit amet,
6901 * consectetur adipiscing elit.
6902 */
6903 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6904 "},
6905 rust_lang.clone(),
6906 &mut cx,
6907 );
6908
6909 // selection w/ non-abutting short block comments
6910 assert_rewrap(
6911 indoc! {"
6912 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6913
6914 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6915 "},
6916 indoc! {"
6917 «/*
6918 * Lorem ipsum dolor sit amet,
6919 * consectetur adipiscing elit.
6920 */
6921
6922 /*
6923 * Lorem ipsum dolor sit amet,
6924 * consectetur adipiscing elit.
6925 */ˇ»
6926 "},
6927 rust_lang.clone(),
6928 &mut cx,
6929 );
6930
6931 // selection of multiline block comments
6932 assert_rewrap(
6933 indoc! {"
6934 «/* Lorem ipsum dolor sit amet,
6935 * consectetur adipiscing elit. */ˇ»
6936 "},
6937 indoc! {"
6938 «/*
6939 * Lorem ipsum dolor sit amet,
6940 * consectetur adipiscing elit.
6941 */ˇ»
6942 "},
6943 rust_lang.clone(),
6944 &mut cx,
6945 );
6946
6947 // partial selection of multiline block comments
6948 assert_rewrap(
6949 indoc! {"
6950 «/* Lorem ipsum dolor sit amet,ˇ»
6951 * consectetur adipiscing elit. */
6952 /* Lorem ipsum dolor sit amet,
6953 «* consectetur adipiscing elit. */ˇ»
6954 "},
6955 indoc! {"
6956 «/*
6957 * Lorem ipsum dolor sit amet,ˇ»
6958 * consectetur adipiscing elit. */
6959 /* Lorem ipsum dolor sit amet,
6960 «* consectetur adipiscing elit.
6961 */ˇ»
6962 "},
6963 rust_lang.clone(),
6964 &mut cx,
6965 );
6966
6967 // selection w/ abutting short block comments
6968 // TODO: should not be combined; should rewrap as 2 comments
6969 assert_rewrap(
6970 indoc! {"
6971 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6972 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6973 "},
6974 // desired behavior:
6975 // indoc! {"
6976 // «/*
6977 // * Lorem ipsum dolor sit amet,
6978 // * consectetur adipiscing elit.
6979 // */
6980 // /*
6981 // * Lorem ipsum dolor sit amet,
6982 // * consectetur adipiscing elit.
6983 // */ˇ»
6984 // "},
6985 // actual behaviour:
6986 indoc! {"
6987 «/*
6988 * Lorem ipsum dolor sit amet,
6989 * consectetur adipiscing elit. Lorem
6990 * ipsum dolor sit amet, consectetur
6991 * adipiscing elit.
6992 */ˇ»
6993 "},
6994 rust_lang.clone(),
6995 &mut cx,
6996 );
6997
6998 // TODO: same as above, but with delimiters on separate line
6999 // assert_rewrap(
7000 // indoc! {"
7001 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7002 // */
7003 // /*
7004 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
7005 // "},
7006 // // desired:
7007 // // indoc! {"
7008 // // «/*
7009 // // * Lorem ipsum dolor sit amet,
7010 // // * consectetur adipiscing elit.
7011 // // */
7012 // // /*
7013 // // * Lorem ipsum dolor sit amet,
7014 // // * consectetur adipiscing elit.
7015 // // */ˇ»
7016 // // "},
7017 // // actual: (but with trailing w/s on the empty lines)
7018 // indoc! {"
7019 // «/*
7020 // * Lorem ipsum dolor sit amet,
7021 // * consectetur adipiscing elit.
7022 // *
7023 // */
7024 // /*
7025 // *
7026 // * Lorem ipsum dolor sit amet,
7027 // * consectetur adipiscing elit.
7028 // */ˇ»
7029 // "},
7030 // rust_lang.clone(),
7031 // &mut cx,
7032 // );
7033
7034 // TODO these are unhandled edge cases; not correct, just documenting known issues
7035 assert_rewrap(
7036 indoc! {"
7037 /*
7038 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7039 */
7040 /*
7041 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7042 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
7043 "},
7044 // desired:
7045 // indoc! {"
7046 // /*
7047 // *ˇ Lorem ipsum dolor sit amet,
7048 // * consectetur adipiscing elit.
7049 // */
7050 // /*
7051 // *ˇ Lorem ipsum dolor sit amet,
7052 // * consectetur adipiscing elit.
7053 // */
7054 // /*
7055 // *ˇ Lorem ipsum dolor sit amet
7056 // */ /* consectetur adipiscing elit. */
7057 // "},
7058 // actual:
7059 indoc! {"
7060 /*
7061 //ˇ Lorem ipsum dolor sit amet,
7062 // consectetur adipiscing elit.
7063 */
7064 /*
7065 * //ˇ Lorem ipsum dolor sit amet,
7066 * consectetur adipiscing elit.
7067 */
7068 /*
7069 *ˇ Lorem ipsum dolor sit amet */ /*
7070 * consectetur adipiscing elit.
7071 */
7072 "},
7073 rust_lang,
7074 &mut cx,
7075 );
7076
7077 #[track_caller]
7078 fn assert_rewrap(
7079 unwrapped_text: &str,
7080 wrapped_text: &str,
7081 language: Arc<Language>,
7082 cx: &mut EditorTestContext,
7083 ) {
7084 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
7085 cx.set_state(unwrapped_text);
7086 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
7087 cx.assert_editor_state(wrapped_text);
7088 }
7089}
7090
7091#[gpui::test]
7092async fn test_hard_wrap(cx: &mut TestAppContext) {
7093 init_test(cx, |_| {});
7094 let mut cx = EditorTestContext::new(cx).await;
7095
7096 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
7097 cx.update_editor(|editor, _, cx| {
7098 editor.set_hard_wrap(Some(14), cx);
7099 });
7100
7101 cx.set_state(indoc!(
7102 "
7103 one two three ˇ
7104 "
7105 ));
7106 cx.simulate_input("four");
7107 cx.run_until_parked();
7108
7109 cx.assert_editor_state(indoc!(
7110 "
7111 one two three
7112 fourˇ
7113 "
7114 ));
7115
7116 cx.update_editor(|editor, window, cx| {
7117 editor.newline(&Default::default(), window, cx);
7118 });
7119 cx.run_until_parked();
7120 cx.assert_editor_state(indoc!(
7121 "
7122 one two three
7123 four
7124 ˇ
7125 "
7126 ));
7127
7128 cx.simulate_input("five");
7129 cx.run_until_parked();
7130 cx.assert_editor_state(indoc!(
7131 "
7132 one two three
7133 four
7134 fiveˇ
7135 "
7136 ));
7137
7138 cx.update_editor(|editor, window, cx| {
7139 editor.newline(&Default::default(), window, cx);
7140 });
7141 cx.run_until_parked();
7142 cx.simulate_input("# ");
7143 cx.run_until_parked();
7144 cx.assert_editor_state(indoc!(
7145 "
7146 one two three
7147 four
7148 five
7149 # ˇ
7150 "
7151 ));
7152
7153 cx.update_editor(|editor, window, cx| {
7154 editor.newline(&Default::default(), window, cx);
7155 });
7156 cx.run_until_parked();
7157 cx.assert_editor_state(indoc!(
7158 "
7159 one two three
7160 four
7161 five
7162 #\x20
7163 #ˇ
7164 "
7165 ));
7166
7167 cx.simulate_input(" 6");
7168 cx.run_until_parked();
7169 cx.assert_editor_state(indoc!(
7170 "
7171 one two three
7172 four
7173 five
7174 #
7175 # 6ˇ
7176 "
7177 ));
7178}
7179
7180#[gpui::test]
7181async fn test_cut_line_ends(cx: &mut TestAppContext) {
7182 init_test(cx, |_| {});
7183
7184 let mut cx = EditorTestContext::new(cx).await;
7185
7186 cx.set_state(indoc! {"The quick brownˇ"});
7187 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7188 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7189
7190 cx.set_state(indoc! {"The emacs foxˇ"});
7191 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7192 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7193
7194 cx.set_state(indoc! {"
7195 The quick« brownˇ»
7196 fox jumps overˇ
7197 the lazy dog"});
7198 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7199 cx.assert_editor_state(indoc! {"
7200 The quickˇ
7201 ˇthe lazy dog"});
7202
7203 cx.set_state(indoc! {"
7204 The quick« brownˇ»
7205 fox jumps overˇ
7206 the lazy dog"});
7207 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7208 cx.assert_editor_state(indoc! {"
7209 The quickˇ
7210 fox jumps overˇthe lazy dog"});
7211
7212 cx.set_state(indoc! {"
7213 The quick« brownˇ»
7214 fox jumps overˇ
7215 the lazy dog"});
7216 cx.update_editor(|e, window, cx| {
7217 e.cut_to_end_of_line(
7218 &CutToEndOfLine {
7219 stop_at_newlines: true,
7220 },
7221 window,
7222 cx,
7223 )
7224 });
7225 cx.assert_editor_state(indoc! {"
7226 The quickˇ
7227 fox jumps overˇ
7228 the lazy dog"});
7229
7230 cx.set_state(indoc! {"
7231 The quick« brownˇ»
7232 fox jumps overˇ
7233 the lazy dog"});
7234 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7235 cx.assert_editor_state(indoc! {"
7236 The quickˇ
7237 fox jumps overˇthe lazy dog"});
7238}
7239
7240#[gpui::test]
7241async fn test_clipboard(cx: &mut TestAppContext) {
7242 init_test(cx, |_| {});
7243
7244 let mut cx = EditorTestContext::new(cx).await;
7245
7246 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7247 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7248 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7249
7250 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7251 cx.set_state("two ˇfour ˇsix ˇ");
7252 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7253 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7254
7255 // Paste again but with only two cursors. Since the number of cursors doesn't
7256 // match the number of slices in the clipboard, the entire clipboard text
7257 // is pasted at each cursor.
7258 cx.set_state("ˇtwo one✅ four three six five ˇ");
7259 cx.update_editor(|e, window, cx| {
7260 e.handle_input("( ", window, cx);
7261 e.paste(&Paste, window, cx);
7262 e.handle_input(") ", window, cx);
7263 });
7264 cx.assert_editor_state(
7265 &([
7266 "( one✅ ",
7267 "three ",
7268 "five ) ˇtwo one✅ four three six five ( one✅ ",
7269 "three ",
7270 "five ) ˇ",
7271 ]
7272 .join("\n")),
7273 );
7274
7275 // Cut with three selections, one of which is full-line.
7276 cx.set_state(indoc! {"
7277 1«2ˇ»3
7278 4ˇ567
7279 «8ˇ»9"});
7280 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7281 cx.assert_editor_state(indoc! {"
7282 1ˇ3
7283 ˇ9"});
7284
7285 // Paste with three selections, noticing how the copied selection that was full-line
7286 // gets inserted before the second cursor.
7287 cx.set_state(indoc! {"
7288 1ˇ3
7289 9ˇ
7290 «oˇ»ne"});
7291 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7292 cx.assert_editor_state(indoc! {"
7293 12ˇ3
7294 4567
7295 9ˇ
7296 8ˇne"});
7297
7298 // Copy with a single cursor only, which writes the whole line into the clipboard.
7299 cx.set_state(indoc! {"
7300 The quick brown
7301 fox juˇmps over
7302 the lazy dog"});
7303 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7304 assert_eq!(
7305 cx.read_from_clipboard()
7306 .and_then(|item| item.text().as_deref().map(str::to_string)),
7307 Some("fox jumps over\n".to_string())
7308 );
7309
7310 // Paste with three selections, noticing how the copied full-line selection is inserted
7311 // before the empty selections but replaces the selection that is non-empty.
7312 cx.set_state(indoc! {"
7313 Tˇhe quick brown
7314 «foˇ»x jumps over
7315 tˇhe lazy dog"});
7316 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7317 cx.assert_editor_state(indoc! {"
7318 fox jumps over
7319 Tˇhe quick brown
7320 fox jumps over
7321 ˇx jumps over
7322 fox jumps over
7323 tˇhe lazy dog"});
7324}
7325
7326#[gpui::test]
7327async fn test_copy_trim(cx: &mut TestAppContext) {
7328 init_test(cx, |_| {});
7329
7330 let mut cx = EditorTestContext::new(cx).await;
7331 cx.set_state(
7332 r#" «for selection in selections.iter() {
7333 let mut start = selection.start;
7334 let mut end = selection.end;
7335 let is_entire_line = selection.is_empty();
7336 if is_entire_line {
7337 start = Point::new(start.row, 0);ˇ»
7338 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7339 }
7340 "#,
7341 );
7342 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7343 assert_eq!(
7344 cx.read_from_clipboard()
7345 .and_then(|item| item.text().as_deref().map(str::to_string)),
7346 Some(
7347 "for selection in selections.iter() {
7348 let mut start = selection.start;
7349 let mut end = selection.end;
7350 let is_entire_line = selection.is_empty();
7351 if is_entire_line {
7352 start = Point::new(start.row, 0);"
7353 .to_string()
7354 ),
7355 "Regular copying preserves all indentation selected",
7356 );
7357 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7358 assert_eq!(
7359 cx.read_from_clipboard()
7360 .and_then(|item| item.text().as_deref().map(str::to_string)),
7361 Some(
7362 "for selection in selections.iter() {
7363let mut start = selection.start;
7364let mut end = selection.end;
7365let is_entire_line = selection.is_empty();
7366if is_entire_line {
7367 start = Point::new(start.row, 0);"
7368 .to_string()
7369 ),
7370 "Copying with stripping should strip all leading whitespaces"
7371 );
7372
7373 cx.set_state(
7374 r#" « for selection in selections.iter() {
7375 let mut start = selection.start;
7376 let mut end = selection.end;
7377 let is_entire_line = selection.is_empty();
7378 if is_entire_line {
7379 start = Point::new(start.row, 0);ˇ»
7380 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7381 }
7382 "#,
7383 );
7384 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7385 assert_eq!(
7386 cx.read_from_clipboard()
7387 .and_then(|item| item.text().as_deref().map(str::to_string)),
7388 Some(
7389 " for selection in selections.iter() {
7390 let mut start = selection.start;
7391 let mut end = selection.end;
7392 let is_entire_line = selection.is_empty();
7393 if is_entire_line {
7394 start = Point::new(start.row, 0);"
7395 .to_string()
7396 ),
7397 "Regular copying preserves all indentation selected",
7398 );
7399 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7400 assert_eq!(
7401 cx.read_from_clipboard()
7402 .and_then(|item| item.text().as_deref().map(str::to_string)),
7403 Some(
7404 "for selection in selections.iter() {
7405let mut start = selection.start;
7406let mut end = selection.end;
7407let is_entire_line = selection.is_empty();
7408if is_entire_line {
7409 start = Point::new(start.row, 0);"
7410 .to_string()
7411 ),
7412 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7413 );
7414
7415 cx.set_state(
7416 r#" «ˇ for selection in selections.iter() {
7417 let mut start = selection.start;
7418 let mut end = selection.end;
7419 let is_entire_line = selection.is_empty();
7420 if is_entire_line {
7421 start = Point::new(start.row, 0);»
7422 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7423 }
7424 "#,
7425 );
7426 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7427 assert_eq!(
7428 cx.read_from_clipboard()
7429 .and_then(|item| item.text().as_deref().map(str::to_string)),
7430 Some(
7431 " for selection in selections.iter() {
7432 let mut start = selection.start;
7433 let mut end = selection.end;
7434 let is_entire_line = selection.is_empty();
7435 if is_entire_line {
7436 start = Point::new(start.row, 0);"
7437 .to_string()
7438 ),
7439 "Regular copying for reverse selection works the same",
7440 );
7441 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7442 assert_eq!(
7443 cx.read_from_clipboard()
7444 .and_then(|item| item.text().as_deref().map(str::to_string)),
7445 Some(
7446 "for selection in selections.iter() {
7447let mut start = selection.start;
7448let mut end = selection.end;
7449let is_entire_line = selection.is_empty();
7450if is_entire_line {
7451 start = Point::new(start.row, 0);"
7452 .to_string()
7453 ),
7454 "Copying with stripping for reverse selection works the same"
7455 );
7456
7457 cx.set_state(
7458 r#" for selection «in selections.iter() {
7459 let mut start = selection.start;
7460 let mut end = selection.end;
7461 let is_entire_line = selection.is_empty();
7462 if is_entire_line {
7463 start = Point::new(start.row, 0);ˇ»
7464 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7465 }
7466 "#,
7467 );
7468 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7469 assert_eq!(
7470 cx.read_from_clipboard()
7471 .and_then(|item| item.text().as_deref().map(str::to_string)),
7472 Some(
7473 "in selections.iter() {
7474 let mut start = selection.start;
7475 let mut end = selection.end;
7476 let is_entire_line = selection.is_empty();
7477 if is_entire_line {
7478 start = Point::new(start.row, 0);"
7479 .to_string()
7480 ),
7481 "When selecting past the indent, the copying works as usual",
7482 );
7483 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7484 assert_eq!(
7485 cx.read_from_clipboard()
7486 .and_then(|item| item.text().as_deref().map(str::to_string)),
7487 Some(
7488 "in selections.iter() {
7489 let mut start = selection.start;
7490 let mut end = selection.end;
7491 let is_entire_line = selection.is_empty();
7492 if is_entire_line {
7493 start = Point::new(start.row, 0);"
7494 .to_string()
7495 ),
7496 "When selecting past the indent, nothing is trimmed"
7497 );
7498
7499 cx.set_state(
7500 r#" «for selection in selections.iter() {
7501 let mut start = selection.start;
7502
7503 let mut end = selection.end;
7504 let is_entire_line = selection.is_empty();
7505 if is_entire_line {
7506 start = Point::new(start.row, 0);
7507ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7508 }
7509 "#,
7510 );
7511 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7512 assert_eq!(
7513 cx.read_from_clipboard()
7514 .and_then(|item| item.text().as_deref().map(str::to_string)),
7515 Some(
7516 "for selection in selections.iter() {
7517let mut start = selection.start;
7518
7519let mut end = selection.end;
7520let is_entire_line = selection.is_empty();
7521if is_entire_line {
7522 start = Point::new(start.row, 0);
7523"
7524 .to_string()
7525 ),
7526 "Copying with stripping should ignore empty lines"
7527 );
7528}
7529
7530#[gpui::test]
7531async fn test_paste_multiline(cx: &mut TestAppContext) {
7532 init_test(cx, |_| {});
7533
7534 let mut cx = EditorTestContext::new(cx).await;
7535 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7536
7537 // Cut an indented block, without the leading whitespace.
7538 cx.set_state(indoc! {"
7539 const a: B = (
7540 c(),
7541 «d(
7542 e,
7543 f
7544 )ˇ»
7545 );
7546 "});
7547 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7548 cx.assert_editor_state(indoc! {"
7549 const a: B = (
7550 c(),
7551 ˇ
7552 );
7553 "});
7554
7555 // Paste it at the same position.
7556 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7557 cx.assert_editor_state(indoc! {"
7558 const a: B = (
7559 c(),
7560 d(
7561 e,
7562 f
7563 )ˇ
7564 );
7565 "});
7566
7567 // Paste it at a line with a lower indent level.
7568 cx.set_state(indoc! {"
7569 ˇ
7570 const a: B = (
7571 c(),
7572 );
7573 "});
7574 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7575 cx.assert_editor_state(indoc! {"
7576 d(
7577 e,
7578 f
7579 )ˇ
7580 const a: B = (
7581 c(),
7582 );
7583 "});
7584
7585 // Cut an indented block, with the leading whitespace.
7586 cx.set_state(indoc! {"
7587 const a: B = (
7588 c(),
7589 « d(
7590 e,
7591 f
7592 )
7593 ˇ»);
7594 "});
7595 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7596 cx.assert_editor_state(indoc! {"
7597 const a: B = (
7598 c(),
7599 ˇ);
7600 "});
7601
7602 // Paste it at the same position.
7603 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7604 cx.assert_editor_state(indoc! {"
7605 const a: B = (
7606 c(),
7607 d(
7608 e,
7609 f
7610 )
7611 ˇ);
7612 "});
7613
7614 // Paste it at a line with a higher indent level.
7615 cx.set_state(indoc! {"
7616 const a: B = (
7617 c(),
7618 d(
7619 e,
7620 fˇ
7621 )
7622 );
7623 "});
7624 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7625 cx.assert_editor_state(indoc! {"
7626 const a: B = (
7627 c(),
7628 d(
7629 e,
7630 f d(
7631 e,
7632 f
7633 )
7634 ˇ
7635 )
7636 );
7637 "});
7638
7639 // Copy an indented block, starting mid-line
7640 cx.set_state(indoc! {"
7641 const a: B = (
7642 c(),
7643 somethin«g(
7644 e,
7645 f
7646 )ˇ»
7647 );
7648 "});
7649 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7650
7651 // Paste it on a line with a lower indent level
7652 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7653 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7654 cx.assert_editor_state(indoc! {"
7655 const a: B = (
7656 c(),
7657 something(
7658 e,
7659 f
7660 )
7661 );
7662 g(
7663 e,
7664 f
7665 )ˇ"});
7666}
7667
7668#[gpui::test]
7669async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7670 init_test(cx, |_| {});
7671
7672 cx.write_to_clipboard(ClipboardItem::new_string(
7673 " d(\n e\n );\n".into(),
7674 ));
7675
7676 let mut cx = EditorTestContext::new(cx).await;
7677 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7678
7679 cx.set_state(indoc! {"
7680 fn a() {
7681 b();
7682 if c() {
7683 ˇ
7684 }
7685 }
7686 "});
7687
7688 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7689 cx.assert_editor_state(indoc! {"
7690 fn a() {
7691 b();
7692 if c() {
7693 d(
7694 e
7695 );
7696 ˇ
7697 }
7698 }
7699 "});
7700
7701 cx.set_state(indoc! {"
7702 fn a() {
7703 b();
7704 ˇ
7705 }
7706 "});
7707
7708 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7709 cx.assert_editor_state(indoc! {"
7710 fn a() {
7711 b();
7712 d(
7713 e
7714 );
7715 ˇ
7716 }
7717 "});
7718}
7719
7720#[gpui::test]
7721fn test_select_all(cx: &mut TestAppContext) {
7722 init_test(cx, |_| {});
7723
7724 let editor = cx.add_window(|window, cx| {
7725 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7726 build_editor(buffer, window, cx)
7727 });
7728 _ = editor.update(cx, |editor, window, cx| {
7729 editor.select_all(&SelectAll, window, cx);
7730 assert_eq!(
7731 display_ranges(editor, cx),
7732 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7733 );
7734 });
7735}
7736
7737#[gpui::test]
7738fn test_select_line(cx: &mut TestAppContext) {
7739 init_test(cx, |_| {});
7740
7741 let editor = cx.add_window(|window, cx| {
7742 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7743 build_editor(buffer, window, cx)
7744 });
7745 _ = editor.update(cx, |editor, window, cx| {
7746 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7747 s.select_display_ranges([
7748 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7749 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7750 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7751 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7752 ])
7753 });
7754 editor.select_line(&SelectLine, window, cx);
7755 // Adjacent line selections should NOT merge (only overlapping ones do)
7756 assert_eq!(
7757 display_ranges(editor, cx),
7758 vec![
7759 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(1), 0),
7760 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(2), 0),
7761 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7762 ]
7763 );
7764 });
7765
7766 _ = editor.update(cx, |editor, window, cx| {
7767 editor.select_line(&SelectLine, window, cx);
7768 assert_eq!(
7769 display_ranges(editor, cx),
7770 vec![
7771 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7772 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7773 ]
7774 );
7775 });
7776
7777 _ = editor.update(cx, |editor, window, cx| {
7778 editor.select_line(&SelectLine, window, cx);
7779 // Adjacent but not overlapping, so they stay separate
7780 assert_eq!(
7781 display_ranges(editor, cx),
7782 vec![
7783 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(4), 0),
7784 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7785 ]
7786 );
7787 });
7788}
7789
7790#[gpui::test]
7791async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7792 init_test(cx, |_| {});
7793 let mut cx = EditorTestContext::new(cx).await;
7794
7795 #[track_caller]
7796 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7797 cx.set_state(initial_state);
7798 cx.update_editor(|e, window, cx| {
7799 e.split_selection_into_lines(&Default::default(), window, cx)
7800 });
7801 cx.assert_editor_state(expected_state);
7802 }
7803
7804 // Selection starts and ends at the middle of lines, left-to-right
7805 test(
7806 &mut cx,
7807 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7808 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7809 );
7810 // Same thing, right-to-left
7811 test(
7812 &mut cx,
7813 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7814 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7815 );
7816
7817 // Whole buffer, left-to-right, last line *doesn't* end with newline
7818 test(
7819 &mut cx,
7820 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7821 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7822 );
7823 // Same thing, right-to-left
7824 test(
7825 &mut cx,
7826 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7827 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7828 );
7829
7830 // Whole buffer, left-to-right, last line ends with newline
7831 test(
7832 &mut cx,
7833 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7834 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7835 );
7836 // Same thing, right-to-left
7837 test(
7838 &mut cx,
7839 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7840 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7841 );
7842
7843 // Starts at the end of a line, ends at the start of another
7844 test(
7845 &mut cx,
7846 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7847 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7848 );
7849}
7850
7851#[gpui::test]
7852async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7853 init_test(cx, |_| {});
7854
7855 let editor = cx.add_window(|window, cx| {
7856 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7857 build_editor(buffer, window, cx)
7858 });
7859
7860 // setup
7861 _ = editor.update(cx, |editor, window, cx| {
7862 editor.fold_creases(
7863 vec![
7864 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7865 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7866 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7867 ],
7868 true,
7869 window,
7870 cx,
7871 );
7872 assert_eq!(
7873 editor.display_text(cx),
7874 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7875 );
7876 });
7877
7878 _ = editor.update(cx, |editor, window, cx| {
7879 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7880 s.select_display_ranges([
7881 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7882 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7883 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7884 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7885 ])
7886 });
7887 editor.split_selection_into_lines(&Default::default(), window, cx);
7888 assert_eq!(
7889 editor.display_text(cx),
7890 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7891 );
7892 });
7893 EditorTestContext::for_editor(editor, cx)
7894 .await
7895 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7896
7897 _ = editor.update(cx, |editor, window, cx| {
7898 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7899 s.select_display_ranges([
7900 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7901 ])
7902 });
7903 editor.split_selection_into_lines(&Default::default(), window, cx);
7904 assert_eq!(
7905 editor.display_text(cx),
7906 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7907 );
7908 assert_eq!(
7909 display_ranges(editor, cx),
7910 [
7911 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7912 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7913 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7914 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7915 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7916 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7917 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7918 ]
7919 );
7920 });
7921 EditorTestContext::for_editor(editor, cx)
7922 .await
7923 .assert_editor_state(
7924 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7925 );
7926}
7927
7928#[gpui::test]
7929async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7930 init_test(cx, |_| {});
7931
7932 let mut cx = EditorTestContext::new(cx).await;
7933
7934 cx.set_state(indoc!(
7935 r#"abc
7936 defˇghi
7937
7938 jk
7939 nlmo
7940 "#
7941 ));
7942
7943 cx.update_editor(|editor, window, cx| {
7944 editor.add_selection_above(&Default::default(), window, cx);
7945 });
7946
7947 cx.assert_editor_state(indoc!(
7948 r#"abcˇ
7949 defˇghi
7950
7951 jk
7952 nlmo
7953 "#
7954 ));
7955
7956 cx.update_editor(|editor, window, cx| {
7957 editor.add_selection_above(&Default::default(), window, cx);
7958 });
7959
7960 cx.assert_editor_state(indoc!(
7961 r#"abcˇ
7962 defˇghi
7963
7964 jk
7965 nlmo
7966 "#
7967 ));
7968
7969 cx.update_editor(|editor, window, cx| {
7970 editor.add_selection_below(&Default::default(), window, cx);
7971 });
7972
7973 cx.assert_editor_state(indoc!(
7974 r#"abc
7975 defˇghi
7976
7977 jk
7978 nlmo
7979 "#
7980 ));
7981
7982 cx.update_editor(|editor, window, cx| {
7983 editor.undo_selection(&Default::default(), window, cx);
7984 });
7985
7986 cx.assert_editor_state(indoc!(
7987 r#"abcˇ
7988 defˇghi
7989
7990 jk
7991 nlmo
7992 "#
7993 ));
7994
7995 cx.update_editor(|editor, window, cx| {
7996 editor.redo_selection(&Default::default(), window, cx);
7997 });
7998
7999 cx.assert_editor_state(indoc!(
8000 r#"abc
8001 defˇghi
8002
8003 jk
8004 nlmo
8005 "#
8006 ));
8007
8008 cx.update_editor(|editor, window, cx| {
8009 editor.add_selection_below(&Default::default(), window, cx);
8010 });
8011
8012 cx.assert_editor_state(indoc!(
8013 r#"abc
8014 defˇghi
8015 ˇ
8016 jk
8017 nlmo
8018 "#
8019 ));
8020
8021 cx.update_editor(|editor, window, cx| {
8022 editor.add_selection_below(&Default::default(), window, cx);
8023 });
8024
8025 cx.assert_editor_state(indoc!(
8026 r#"abc
8027 defˇghi
8028 ˇ
8029 jkˇ
8030 nlmo
8031 "#
8032 ));
8033
8034 cx.update_editor(|editor, window, cx| {
8035 editor.add_selection_below(&Default::default(), window, cx);
8036 });
8037
8038 cx.assert_editor_state(indoc!(
8039 r#"abc
8040 defˇghi
8041 ˇ
8042 jkˇ
8043 nlmˇo
8044 "#
8045 ));
8046
8047 cx.update_editor(|editor, window, cx| {
8048 editor.add_selection_below(&Default::default(), window, cx);
8049 });
8050
8051 cx.assert_editor_state(indoc!(
8052 r#"abc
8053 defˇghi
8054 ˇ
8055 jkˇ
8056 nlmˇo
8057 ˇ"#
8058 ));
8059
8060 // change selections
8061 cx.set_state(indoc!(
8062 r#"abc
8063 def«ˇg»hi
8064
8065 jk
8066 nlmo
8067 "#
8068 ));
8069
8070 cx.update_editor(|editor, window, cx| {
8071 editor.add_selection_below(&Default::default(), window, cx);
8072 });
8073
8074 cx.assert_editor_state(indoc!(
8075 r#"abc
8076 def«ˇg»hi
8077
8078 jk
8079 nlm«ˇo»
8080 "#
8081 ));
8082
8083 cx.update_editor(|editor, window, cx| {
8084 editor.add_selection_below(&Default::default(), window, cx);
8085 });
8086
8087 cx.assert_editor_state(indoc!(
8088 r#"abc
8089 def«ˇg»hi
8090
8091 jk
8092 nlm«ˇo»
8093 "#
8094 ));
8095
8096 cx.update_editor(|editor, window, cx| {
8097 editor.add_selection_above(&Default::default(), window, cx);
8098 });
8099
8100 cx.assert_editor_state(indoc!(
8101 r#"abc
8102 def«ˇg»hi
8103
8104 jk
8105 nlmo
8106 "#
8107 ));
8108
8109 cx.update_editor(|editor, window, cx| {
8110 editor.add_selection_above(&Default::default(), window, cx);
8111 });
8112
8113 cx.assert_editor_state(indoc!(
8114 r#"abc
8115 def«ˇg»hi
8116
8117 jk
8118 nlmo
8119 "#
8120 ));
8121
8122 // Change selections again
8123 cx.set_state(indoc!(
8124 r#"a«bc
8125 defgˇ»hi
8126
8127 jk
8128 nlmo
8129 "#
8130 ));
8131
8132 cx.update_editor(|editor, window, cx| {
8133 editor.add_selection_below(&Default::default(), window, cx);
8134 });
8135
8136 cx.assert_editor_state(indoc!(
8137 r#"a«bcˇ»
8138 d«efgˇ»hi
8139
8140 j«kˇ»
8141 nlmo
8142 "#
8143 ));
8144
8145 cx.update_editor(|editor, window, cx| {
8146 editor.add_selection_below(&Default::default(), window, cx);
8147 });
8148 cx.assert_editor_state(indoc!(
8149 r#"a«bcˇ»
8150 d«efgˇ»hi
8151
8152 j«kˇ»
8153 n«lmoˇ»
8154 "#
8155 ));
8156 cx.update_editor(|editor, window, cx| {
8157 editor.add_selection_above(&Default::default(), window, cx);
8158 });
8159
8160 cx.assert_editor_state(indoc!(
8161 r#"a«bcˇ»
8162 d«efgˇ»hi
8163
8164 j«kˇ»
8165 nlmo
8166 "#
8167 ));
8168
8169 // Change selections again
8170 cx.set_state(indoc!(
8171 r#"abc
8172 d«ˇefghi
8173
8174 jk
8175 nlm»o
8176 "#
8177 ));
8178
8179 cx.update_editor(|editor, window, cx| {
8180 editor.add_selection_above(&Default::default(), window, cx);
8181 });
8182
8183 cx.assert_editor_state(indoc!(
8184 r#"a«ˇbc»
8185 d«ˇef»ghi
8186
8187 j«ˇk»
8188 n«ˇlm»o
8189 "#
8190 ));
8191
8192 cx.update_editor(|editor, window, cx| {
8193 editor.add_selection_below(&Default::default(), window, cx);
8194 });
8195
8196 cx.assert_editor_state(indoc!(
8197 r#"abc
8198 d«ˇef»ghi
8199
8200 j«ˇk»
8201 n«ˇlm»o
8202 "#
8203 ));
8204}
8205
8206#[gpui::test]
8207async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8208 init_test(cx, |_| {});
8209 let mut cx = EditorTestContext::new(cx).await;
8210
8211 cx.set_state(indoc!(
8212 r#"line onˇe
8213 liˇne two
8214 line three
8215 line four"#
8216 ));
8217
8218 cx.update_editor(|editor, window, cx| {
8219 editor.add_selection_below(&Default::default(), window, cx);
8220 });
8221
8222 // test multiple cursors expand in the same direction
8223 cx.assert_editor_state(indoc!(
8224 r#"line onˇe
8225 liˇne twˇo
8226 liˇne three
8227 line four"#
8228 ));
8229
8230 cx.update_editor(|editor, window, cx| {
8231 editor.add_selection_below(&Default::default(), window, cx);
8232 });
8233
8234 cx.update_editor(|editor, window, cx| {
8235 editor.add_selection_below(&Default::default(), window, cx);
8236 });
8237
8238 // test multiple cursors expand below overflow
8239 cx.assert_editor_state(indoc!(
8240 r#"line onˇe
8241 liˇne twˇo
8242 liˇne thˇree
8243 liˇne foˇur"#
8244 ));
8245
8246 cx.update_editor(|editor, window, cx| {
8247 editor.add_selection_above(&Default::default(), window, cx);
8248 });
8249
8250 // test multiple cursors retrieves back correctly
8251 cx.assert_editor_state(indoc!(
8252 r#"line onˇe
8253 liˇne twˇo
8254 liˇne thˇree
8255 line four"#
8256 ));
8257
8258 cx.update_editor(|editor, window, cx| {
8259 editor.add_selection_above(&Default::default(), window, cx);
8260 });
8261
8262 cx.update_editor(|editor, window, cx| {
8263 editor.add_selection_above(&Default::default(), window, cx);
8264 });
8265
8266 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8267 cx.assert_editor_state(indoc!(
8268 r#"liˇne onˇe
8269 liˇne two
8270 line three
8271 line four"#
8272 ));
8273
8274 cx.update_editor(|editor, window, cx| {
8275 editor.undo_selection(&Default::default(), window, cx);
8276 });
8277
8278 // test undo
8279 cx.assert_editor_state(indoc!(
8280 r#"line onˇe
8281 liˇne twˇo
8282 line three
8283 line four"#
8284 ));
8285
8286 cx.update_editor(|editor, window, cx| {
8287 editor.redo_selection(&Default::default(), window, cx);
8288 });
8289
8290 // test redo
8291 cx.assert_editor_state(indoc!(
8292 r#"liˇne onˇe
8293 liˇne two
8294 line three
8295 line four"#
8296 ));
8297
8298 cx.set_state(indoc!(
8299 r#"abcd
8300 ef«ghˇ»
8301 ijkl
8302 «mˇ»nop"#
8303 ));
8304
8305 cx.update_editor(|editor, window, cx| {
8306 editor.add_selection_above(&Default::default(), window, cx);
8307 });
8308
8309 // test multiple selections expand in the same direction
8310 cx.assert_editor_state(indoc!(
8311 r#"ab«cdˇ»
8312 ef«ghˇ»
8313 «iˇ»jkl
8314 «mˇ»nop"#
8315 ));
8316
8317 cx.update_editor(|editor, window, cx| {
8318 editor.add_selection_above(&Default::default(), window, cx);
8319 });
8320
8321 // test multiple selection upward overflow
8322 cx.assert_editor_state(indoc!(
8323 r#"ab«cdˇ»
8324 «eˇ»f«ghˇ»
8325 «iˇ»jkl
8326 «mˇ»nop"#
8327 ));
8328
8329 cx.update_editor(|editor, window, cx| {
8330 editor.add_selection_below(&Default::default(), window, cx);
8331 });
8332
8333 // test multiple selection retrieves back correctly
8334 cx.assert_editor_state(indoc!(
8335 r#"abcd
8336 ef«ghˇ»
8337 «iˇ»jkl
8338 «mˇ»nop"#
8339 ));
8340
8341 cx.update_editor(|editor, window, cx| {
8342 editor.add_selection_below(&Default::default(), window, cx);
8343 });
8344
8345 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8346 cx.assert_editor_state(indoc!(
8347 r#"abcd
8348 ef«ghˇ»
8349 ij«klˇ»
8350 «mˇ»nop"#
8351 ));
8352
8353 cx.update_editor(|editor, window, cx| {
8354 editor.undo_selection(&Default::default(), window, cx);
8355 });
8356
8357 // test undo
8358 cx.assert_editor_state(indoc!(
8359 r#"abcd
8360 ef«ghˇ»
8361 «iˇ»jkl
8362 «mˇ»nop"#
8363 ));
8364
8365 cx.update_editor(|editor, window, cx| {
8366 editor.redo_selection(&Default::default(), window, cx);
8367 });
8368
8369 // test redo
8370 cx.assert_editor_state(indoc!(
8371 r#"abcd
8372 ef«ghˇ»
8373 ij«klˇ»
8374 «mˇ»nop"#
8375 ));
8376}
8377
8378#[gpui::test]
8379async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8380 init_test(cx, |_| {});
8381 let mut cx = EditorTestContext::new(cx).await;
8382
8383 cx.set_state(indoc!(
8384 r#"line onˇe
8385 liˇne two
8386 line three
8387 line four"#
8388 ));
8389
8390 cx.update_editor(|editor, window, cx| {
8391 editor.add_selection_below(&Default::default(), window, cx);
8392 editor.add_selection_below(&Default::default(), window, cx);
8393 editor.add_selection_below(&Default::default(), window, cx);
8394 });
8395
8396 // initial state with two multi cursor groups
8397 cx.assert_editor_state(indoc!(
8398 r#"line onˇe
8399 liˇne twˇo
8400 liˇne thˇree
8401 liˇne foˇur"#
8402 ));
8403
8404 // add single cursor in middle - simulate opt click
8405 cx.update_editor(|editor, window, cx| {
8406 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8407 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8408 editor.end_selection(window, cx);
8409 });
8410
8411 cx.assert_editor_state(indoc!(
8412 r#"line onˇe
8413 liˇne twˇo
8414 liˇneˇ thˇree
8415 liˇne foˇur"#
8416 ));
8417
8418 cx.update_editor(|editor, window, cx| {
8419 editor.add_selection_above(&Default::default(), window, cx);
8420 });
8421
8422 // test new added selection expands above and existing selection shrinks
8423 cx.assert_editor_state(indoc!(
8424 r#"line onˇe
8425 liˇneˇ twˇo
8426 liˇneˇ thˇree
8427 line four"#
8428 ));
8429
8430 cx.update_editor(|editor, window, cx| {
8431 editor.add_selection_above(&Default::default(), window, cx);
8432 });
8433
8434 // test new added selection expands above and existing selection shrinks
8435 cx.assert_editor_state(indoc!(
8436 r#"lineˇ onˇe
8437 liˇneˇ twˇo
8438 lineˇ three
8439 line four"#
8440 ));
8441
8442 // intial state with two selection groups
8443 cx.set_state(indoc!(
8444 r#"abcd
8445 ef«ghˇ»
8446 ijkl
8447 «mˇ»nop"#
8448 ));
8449
8450 cx.update_editor(|editor, window, cx| {
8451 editor.add_selection_above(&Default::default(), window, cx);
8452 editor.add_selection_above(&Default::default(), window, cx);
8453 });
8454
8455 cx.assert_editor_state(indoc!(
8456 r#"ab«cdˇ»
8457 «eˇ»f«ghˇ»
8458 «iˇ»jkl
8459 «mˇ»nop"#
8460 ));
8461
8462 // add single selection in middle - simulate opt drag
8463 cx.update_editor(|editor, window, cx| {
8464 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8465 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8466 editor.update_selection(
8467 DisplayPoint::new(DisplayRow(2), 4),
8468 0,
8469 gpui::Point::<f32>::default(),
8470 window,
8471 cx,
8472 );
8473 editor.end_selection(window, cx);
8474 });
8475
8476 cx.assert_editor_state(indoc!(
8477 r#"ab«cdˇ»
8478 «eˇ»f«ghˇ»
8479 «iˇ»jk«lˇ»
8480 «mˇ»nop"#
8481 ));
8482
8483 cx.update_editor(|editor, window, cx| {
8484 editor.add_selection_below(&Default::default(), window, cx);
8485 });
8486
8487 // test new added selection expands below, others shrinks from above
8488 cx.assert_editor_state(indoc!(
8489 r#"abcd
8490 ef«ghˇ»
8491 «iˇ»jk«lˇ»
8492 «mˇ»no«pˇ»"#
8493 ));
8494}
8495
8496#[gpui::test]
8497async fn test_select_next(cx: &mut TestAppContext) {
8498 init_test(cx, |_| {});
8499 let mut cx = EditorTestContext::new(cx).await;
8500
8501 // Enable case sensitive search.
8502 update_test_editor_settings(&mut cx, |settings| {
8503 let mut search_settings = SearchSettingsContent::default();
8504 search_settings.case_sensitive = Some(true);
8505 settings.search = Some(search_settings);
8506 });
8507
8508 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8509
8510 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8511 .unwrap();
8512 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8513
8514 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8515 .unwrap();
8516 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8517
8518 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8519 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8520
8521 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8522 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8523
8524 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8525 .unwrap();
8526 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8527
8528 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8529 .unwrap();
8530 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8531
8532 // Test selection direction should be preserved
8533 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8534
8535 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8536 .unwrap();
8537 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8538
8539 // Test case sensitivity
8540 cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
8541 cx.update_editor(|e, window, cx| {
8542 e.select_next(&SelectNext::default(), window, cx).unwrap();
8543 });
8544 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8545
8546 // Disable case sensitive search.
8547 update_test_editor_settings(&mut cx, |settings| {
8548 let mut search_settings = SearchSettingsContent::default();
8549 search_settings.case_sensitive = Some(false);
8550 settings.search = Some(search_settings);
8551 });
8552
8553 cx.set_state("«ˇfoo»\nFOO\nFoo");
8554 cx.update_editor(|e, window, cx| {
8555 e.select_next(&SelectNext::default(), window, cx).unwrap();
8556 e.select_next(&SelectNext::default(), window, cx).unwrap();
8557 });
8558 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8559}
8560
8561#[gpui::test]
8562async fn test_select_all_matches(cx: &mut TestAppContext) {
8563 init_test(cx, |_| {});
8564 let mut cx = EditorTestContext::new(cx).await;
8565
8566 // Enable case sensitive search.
8567 update_test_editor_settings(&mut cx, |settings| {
8568 let mut search_settings = SearchSettingsContent::default();
8569 search_settings.case_sensitive = Some(true);
8570 settings.search = Some(search_settings);
8571 });
8572
8573 // Test caret-only selections
8574 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8575 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8576 .unwrap();
8577 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8578
8579 // Test left-to-right selections
8580 cx.set_state("abc\n«abcˇ»\nabc");
8581 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8582 .unwrap();
8583 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8584
8585 // Test right-to-left selections
8586 cx.set_state("abc\n«ˇabc»\nabc");
8587 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8588 .unwrap();
8589 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8590
8591 // Test selecting whitespace with caret selection
8592 cx.set_state("abc\nˇ abc\nabc");
8593 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8594 .unwrap();
8595 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8596
8597 // Test selecting whitespace with left-to-right selection
8598 cx.set_state("abc\n«ˇ »abc\nabc");
8599 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8600 .unwrap();
8601 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8602
8603 // Test no matches with right-to-left selection
8604 cx.set_state("abc\n« ˇ»abc\nabc");
8605 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8606 .unwrap();
8607 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8608
8609 // Test with a single word and clip_at_line_ends=true (#29823)
8610 cx.set_state("aˇbc");
8611 cx.update_editor(|e, window, cx| {
8612 e.set_clip_at_line_ends(true, cx);
8613 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8614 e.set_clip_at_line_ends(false, cx);
8615 });
8616 cx.assert_editor_state("«abcˇ»");
8617
8618 // Test case sensitivity
8619 cx.set_state("fˇoo\nFOO\nFoo");
8620 cx.update_editor(|e, window, cx| {
8621 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8622 });
8623 cx.assert_editor_state("«fooˇ»\nFOO\nFoo");
8624
8625 // Disable case sensitive search.
8626 update_test_editor_settings(&mut cx, |settings| {
8627 let mut search_settings = SearchSettingsContent::default();
8628 search_settings.case_sensitive = Some(false);
8629 settings.search = Some(search_settings);
8630 });
8631
8632 cx.set_state("fˇoo\nFOO\nFoo");
8633 cx.update_editor(|e, window, cx| {
8634 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8635 });
8636 cx.assert_editor_state("«fooˇ»\n«FOOˇ»\n«Fooˇ»");
8637}
8638
8639#[gpui::test]
8640async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8641 init_test(cx, |_| {});
8642
8643 let mut cx = EditorTestContext::new(cx).await;
8644
8645 let large_body_1 = "\nd".repeat(200);
8646 let large_body_2 = "\ne".repeat(200);
8647
8648 cx.set_state(&format!(
8649 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8650 ));
8651 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8652 let scroll_position = editor.scroll_position(cx);
8653 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8654 scroll_position
8655 });
8656
8657 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8658 .unwrap();
8659 cx.assert_editor_state(&format!(
8660 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8661 ));
8662 let scroll_position_after_selection =
8663 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8664 assert_eq!(
8665 initial_scroll_position, scroll_position_after_selection,
8666 "Scroll position should not change after selecting all matches"
8667 );
8668}
8669
8670#[gpui::test]
8671async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8672 init_test(cx, |_| {});
8673
8674 let mut cx = EditorLspTestContext::new_rust(
8675 lsp::ServerCapabilities {
8676 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8677 ..Default::default()
8678 },
8679 cx,
8680 )
8681 .await;
8682
8683 cx.set_state(indoc! {"
8684 line 1
8685 line 2
8686 linˇe 3
8687 line 4
8688 line 5
8689 "});
8690
8691 // Make an edit
8692 cx.update_editor(|editor, window, cx| {
8693 editor.handle_input("X", window, cx);
8694 });
8695
8696 // Move cursor to a different position
8697 cx.update_editor(|editor, window, cx| {
8698 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8699 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8700 });
8701 });
8702
8703 cx.assert_editor_state(indoc! {"
8704 line 1
8705 line 2
8706 linXe 3
8707 line 4
8708 liˇne 5
8709 "});
8710
8711 cx.lsp
8712 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8713 Ok(Some(vec![lsp::TextEdit::new(
8714 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8715 "PREFIX ".to_string(),
8716 )]))
8717 });
8718
8719 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8720 .unwrap()
8721 .await
8722 .unwrap();
8723
8724 cx.assert_editor_state(indoc! {"
8725 PREFIX line 1
8726 line 2
8727 linXe 3
8728 line 4
8729 liˇne 5
8730 "});
8731
8732 // Undo formatting
8733 cx.update_editor(|editor, window, cx| {
8734 editor.undo(&Default::default(), window, cx);
8735 });
8736
8737 // Verify cursor moved back to position after edit
8738 cx.assert_editor_state(indoc! {"
8739 line 1
8740 line 2
8741 linXˇe 3
8742 line 4
8743 line 5
8744 "});
8745}
8746
8747#[gpui::test]
8748async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8749 init_test(cx, |_| {});
8750
8751 let mut cx = EditorTestContext::new(cx).await;
8752
8753 let provider = cx.new(|_| FakeEditPredictionDelegate::default());
8754 cx.update_editor(|editor, window, cx| {
8755 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8756 });
8757
8758 cx.set_state(indoc! {"
8759 line 1
8760 line 2
8761 linˇe 3
8762 line 4
8763 line 5
8764 line 6
8765 line 7
8766 line 8
8767 line 9
8768 line 10
8769 "});
8770
8771 let snapshot = cx.buffer_snapshot();
8772 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8773
8774 cx.update(|_, cx| {
8775 provider.update(cx, |provider, _| {
8776 provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local {
8777 id: None,
8778 edits: vec![(edit_position..edit_position, "X".into())],
8779 edit_preview: None,
8780 }))
8781 })
8782 });
8783
8784 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8785 cx.update_editor(|editor, window, cx| {
8786 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8787 });
8788
8789 cx.assert_editor_state(indoc! {"
8790 line 1
8791 line 2
8792 lineXˇ 3
8793 line 4
8794 line 5
8795 line 6
8796 line 7
8797 line 8
8798 line 9
8799 line 10
8800 "});
8801
8802 cx.update_editor(|editor, window, cx| {
8803 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8804 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8805 });
8806 });
8807
8808 cx.assert_editor_state(indoc! {"
8809 line 1
8810 line 2
8811 lineX 3
8812 line 4
8813 line 5
8814 line 6
8815 line 7
8816 line 8
8817 line 9
8818 liˇne 10
8819 "});
8820
8821 cx.update_editor(|editor, window, cx| {
8822 editor.undo(&Default::default(), window, cx);
8823 });
8824
8825 cx.assert_editor_state(indoc! {"
8826 line 1
8827 line 2
8828 lineˇ 3
8829 line 4
8830 line 5
8831 line 6
8832 line 7
8833 line 8
8834 line 9
8835 line 10
8836 "});
8837}
8838
8839#[gpui::test]
8840async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8841 init_test(cx, |_| {});
8842
8843 let mut cx = EditorTestContext::new(cx).await;
8844 cx.set_state(
8845 r#"let foo = 2;
8846lˇet foo = 2;
8847let fooˇ = 2;
8848let foo = 2;
8849let foo = ˇ2;"#,
8850 );
8851
8852 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8853 .unwrap();
8854 cx.assert_editor_state(
8855 r#"let foo = 2;
8856«letˇ» foo = 2;
8857let «fooˇ» = 2;
8858let foo = 2;
8859let foo = «2ˇ»;"#,
8860 );
8861
8862 // noop for multiple selections with different contents
8863 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8864 .unwrap();
8865 cx.assert_editor_state(
8866 r#"let foo = 2;
8867«letˇ» foo = 2;
8868let «fooˇ» = 2;
8869let foo = 2;
8870let foo = «2ˇ»;"#,
8871 );
8872
8873 // Test last selection direction should be preserved
8874 cx.set_state(
8875 r#"let foo = 2;
8876let foo = 2;
8877let «fooˇ» = 2;
8878let «ˇfoo» = 2;
8879let foo = 2;"#,
8880 );
8881
8882 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8883 .unwrap();
8884 cx.assert_editor_state(
8885 r#"let foo = 2;
8886let foo = 2;
8887let «fooˇ» = 2;
8888let «ˇfoo» = 2;
8889let «ˇfoo» = 2;"#,
8890 );
8891}
8892
8893#[gpui::test]
8894async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8895 init_test(cx, |_| {});
8896
8897 let mut cx =
8898 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8899
8900 cx.assert_editor_state(indoc! {"
8901 ˇbbb
8902 ccc
8903
8904 bbb
8905 ccc
8906 "});
8907 cx.dispatch_action(SelectPrevious::default());
8908 cx.assert_editor_state(indoc! {"
8909 «bbbˇ»
8910 ccc
8911
8912 bbb
8913 ccc
8914 "});
8915 cx.dispatch_action(SelectPrevious::default());
8916 cx.assert_editor_state(indoc! {"
8917 «bbbˇ»
8918 ccc
8919
8920 «bbbˇ»
8921 ccc
8922 "});
8923}
8924
8925#[gpui::test]
8926async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8927 init_test(cx, |_| {});
8928
8929 let mut cx = EditorTestContext::new(cx).await;
8930 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8931
8932 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8933 .unwrap();
8934 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8935
8936 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8937 .unwrap();
8938 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8939
8940 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8941 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8942
8943 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8944 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8945
8946 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8947 .unwrap();
8948 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8949
8950 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8951 .unwrap();
8952 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8953}
8954
8955#[gpui::test]
8956async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8957 init_test(cx, |_| {});
8958
8959 let mut cx = EditorTestContext::new(cx).await;
8960 cx.set_state("aˇ");
8961
8962 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8963 .unwrap();
8964 cx.assert_editor_state("«aˇ»");
8965 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8966 .unwrap();
8967 cx.assert_editor_state("«aˇ»");
8968}
8969
8970#[gpui::test]
8971async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8972 init_test(cx, |_| {});
8973
8974 let mut cx = EditorTestContext::new(cx).await;
8975 cx.set_state(
8976 r#"let foo = 2;
8977lˇet foo = 2;
8978let fooˇ = 2;
8979let foo = 2;
8980let foo = ˇ2;"#,
8981 );
8982
8983 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8984 .unwrap();
8985 cx.assert_editor_state(
8986 r#"let foo = 2;
8987«letˇ» foo = 2;
8988let «fooˇ» = 2;
8989let foo = 2;
8990let foo = «2ˇ»;"#,
8991 );
8992
8993 // noop for multiple selections with different contents
8994 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8995 .unwrap();
8996 cx.assert_editor_state(
8997 r#"let foo = 2;
8998«letˇ» foo = 2;
8999let «fooˇ» = 2;
9000let foo = 2;
9001let foo = «2ˇ»;"#,
9002 );
9003}
9004
9005#[gpui::test]
9006async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
9007 init_test(cx, |_| {});
9008 let mut cx = EditorTestContext::new(cx).await;
9009
9010 // Enable case sensitive search.
9011 update_test_editor_settings(&mut cx, |settings| {
9012 let mut search_settings = SearchSettingsContent::default();
9013 search_settings.case_sensitive = Some(true);
9014 settings.search = Some(search_settings);
9015 });
9016
9017 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
9018
9019 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9020 .unwrap();
9021 // selection direction is preserved
9022 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
9023
9024 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9025 .unwrap();
9026 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
9027
9028 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
9029 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
9030
9031 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
9032 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
9033
9034 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9035 .unwrap();
9036 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
9037
9038 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9039 .unwrap();
9040 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
9041
9042 // Test case sensitivity
9043 cx.set_state("foo\nFOO\nFoo\n«ˇfoo»");
9044 cx.update_editor(|e, window, cx| {
9045 e.select_previous(&SelectPrevious::default(), window, cx)
9046 .unwrap();
9047 e.select_previous(&SelectPrevious::default(), window, cx)
9048 .unwrap();
9049 });
9050 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
9051
9052 // Disable case sensitive search.
9053 update_test_editor_settings(&mut cx, |settings| {
9054 let mut search_settings = SearchSettingsContent::default();
9055 search_settings.case_sensitive = Some(false);
9056 settings.search = Some(search_settings);
9057 });
9058
9059 cx.set_state("foo\nFOO\n«ˇFoo»");
9060 cx.update_editor(|e, window, cx| {
9061 e.select_previous(&SelectPrevious::default(), window, cx)
9062 .unwrap();
9063 e.select_previous(&SelectPrevious::default(), window, cx)
9064 .unwrap();
9065 });
9066 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
9067}
9068
9069#[gpui::test]
9070async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
9071 init_test(cx, |_| {});
9072
9073 let language = Arc::new(Language::new(
9074 LanguageConfig::default(),
9075 Some(tree_sitter_rust::LANGUAGE.into()),
9076 ));
9077
9078 let text = r#"
9079 use mod1::mod2::{mod3, mod4};
9080
9081 fn fn_1(param1: bool, param2: &str) {
9082 let var1 = "text";
9083 }
9084 "#
9085 .unindent();
9086
9087 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9088 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9089 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9090
9091 editor
9092 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9093 .await;
9094
9095 editor.update_in(cx, |editor, window, cx| {
9096 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9097 s.select_display_ranges([
9098 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
9099 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
9100 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
9101 ]);
9102 });
9103 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9104 });
9105 editor.update(cx, |editor, cx| {
9106 assert_text_with_selections(
9107 editor,
9108 indoc! {r#"
9109 use mod1::mod2::{mod3, «mod4ˇ»};
9110
9111 fn fn_1«ˇ(param1: bool, param2: &str)» {
9112 let var1 = "«ˇtext»";
9113 }
9114 "#},
9115 cx,
9116 );
9117 });
9118
9119 editor.update_in(cx, |editor, window, cx| {
9120 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9121 });
9122 editor.update(cx, |editor, cx| {
9123 assert_text_with_selections(
9124 editor,
9125 indoc! {r#"
9126 use mod1::mod2::«{mod3, mod4}ˇ»;
9127
9128 «ˇfn fn_1(param1: bool, param2: &str) {
9129 let var1 = "text";
9130 }»
9131 "#},
9132 cx,
9133 );
9134 });
9135
9136 editor.update_in(cx, |editor, window, cx| {
9137 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9138 });
9139 assert_eq!(
9140 editor.update(cx, |editor, cx| editor
9141 .selections
9142 .display_ranges(&editor.display_snapshot(cx))),
9143 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9144 );
9145
9146 // Trying to expand the selected syntax node one more time has no effect.
9147 editor.update_in(cx, |editor, window, cx| {
9148 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9149 });
9150 assert_eq!(
9151 editor.update(cx, |editor, cx| editor
9152 .selections
9153 .display_ranges(&editor.display_snapshot(cx))),
9154 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9155 );
9156
9157 editor.update_in(cx, |editor, window, cx| {
9158 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9159 });
9160 editor.update(cx, |editor, cx| {
9161 assert_text_with_selections(
9162 editor,
9163 indoc! {r#"
9164 use mod1::mod2::«{mod3, mod4}ˇ»;
9165
9166 «ˇfn fn_1(param1: bool, param2: &str) {
9167 let var1 = "text";
9168 }»
9169 "#},
9170 cx,
9171 );
9172 });
9173
9174 editor.update_in(cx, |editor, window, cx| {
9175 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9176 });
9177 editor.update(cx, |editor, cx| {
9178 assert_text_with_selections(
9179 editor,
9180 indoc! {r#"
9181 use mod1::mod2::{mod3, «mod4ˇ»};
9182
9183 fn fn_1«ˇ(param1: bool, param2: &str)» {
9184 let var1 = "«ˇtext»";
9185 }
9186 "#},
9187 cx,
9188 );
9189 });
9190
9191 editor.update_in(cx, |editor, window, cx| {
9192 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9193 });
9194 editor.update(cx, |editor, cx| {
9195 assert_text_with_selections(
9196 editor,
9197 indoc! {r#"
9198 use mod1::mod2::{mod3, moˇd4};
9199
9200 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9201 let var1 = "teˇxt";
9202 }
9203 "#},
9204 cx,
9205 );
9206 });
9207
9208 // Trying to shrink the selected syntax node one more time has no effect.
9209 editor.update_in(cx, |editor, window, cx| {
9210 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9211 });
9212 editor.update_in(cx, |editor, _, cx| {
9213 assert_text_with_selections(
9214 editor,
9215 indoc! {r#"
9216 use mod1::mod2::{mod3, moˇd4};
9217
9218 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9219 let var1 = "teˇxt";
9220 }
9221 "#},
9222 cx,
9223 );
9224 });
9225
9226 // Ensure that we keep expanding the selection if the larger selection starts or ends within
9227 // a fold.
9228 editor.update_in(cx, |editor, window, cx| {
9229 editor.fold_creases(
9230 vec![
9231 Crease::simple(
9232 Point::new(0, 21)..Point::new(0, 24),
9233 FoldPlaceholder::test(),
9234 ),
9235 Crease::simple(
9236 Point::new(3, 20)..Point::new(3, 22),
9237 FoldPlaceholder::test(),
9238 ),
9239 ],
9240 true,
9241 window,
9242 cx,
9243 );
9244 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9245 });
9246 editor.update(cx, |editor, cx| {
9247 assert_text_with_selections(
9248 editor,
9249 indoc! {r#"
9250 use mod1::mod2::«{mod3, mod4}ˇ»;
9251
9252 fn fn_1«ˇ(param1: bool, param2: &str)» {
9253 let var1 = "«ˇtext»";
9254 }
9255 "#},
9256 cx,
9257 );
9258 });
9259}
9260
9261#[gpui::test]
9262async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
9263 init_test(cx, |_| {});
9264
9265 let language = Arc::new(Language::new(
9266 LanguageConfig::default(),
9267 Some(tree_sitter_rust::LANGUAGE.into()),
9268 ));
9269
9270 let text = "let a = 2;";
9271
9272 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9273 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9274 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9275
9276 editor
9277 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9278 .await;
9279
9280 // Test case 1: Cursor at end of word
9281 editor.update_in(cx, |editor, window, cx| {
9282 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9283 s.select_display_ranges([
9284 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9285 ]);
9286 });
9287 });
9288 editor.update(cx, |editor, cx| {
9289 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9290 });
9291 editor.update_in(cx, |editor, window, cx| {
9292 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9293 });
9294 editor.update(cx, |editor, cx| {
9295 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9296 });
9297 editor.update_in(cx, |editor, window, cx| {
9298 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9299 });
9300 editor.update(cx, |editor, cx| {
9301 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9302 });
9303
9304 // Test case 2: Cursor at end of statement
9305 editor.update_in(cx, |editor, window, cx| {
9306 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9307 s.select_display_ranges([
9308 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9309 ]);
9310 });
9311 });
9312 editor.update(cx, |editor, cx| {
9313 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9314 });
9315 editor.update_in(cx, |editor, window, cx| {
9316 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9317 });
9318 editor.update(cx, |editor, cx| {
9319 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9320 });
9321}
9322
9323#[gpui::test]
9324async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9325 init_test(cx, |_| {});
9326
9327 let language = Arc::new(Language::new(
9328 LanguageConfig {
9329 name: "JavaScript".into(),
9330 ..Default::default()
9331 },
9332 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9333 ));
9334
9335 let text = r#"
9336 let a = {
9337 key: "value",
9338 };
9339 "#
9340 .unindent();
9341
9342 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9343 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9344 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9345
9346 editor
9347 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9348 .await;
9349
9350 // Test case 1: Cursor after '{'
9351 editor.update_in(cx, |editor, window, cx| {
9352 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9353 s.select_display_ranges([
9354 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9355 ]);
9356 });
9357 });
9358 editor.update(cx, |editor, cx| {
9359 assert_text_with_selections(
9360 editor,
9361 indoc! {r#"
9362 let a = {ˇ
9363 key: "value",
9364 };
9365 "#},
9366 cx,
9367 );
9368 });
9369 editor.update_in(cx, |editor, window, cx| {
9370 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9371 });
9372 editor.update(cx, |editor, cx| {
9373 assert_text_with_selections(
9374 editor,
9375 indoc! {r#"
9376 let a = «ˇ{
9377 key: "value",
9378 }»;
9379 "#},
9380 cx,
9381 );
9382 });
9383
9384 // Test case 2: Cursor after ':'
9385 editor.update_in(cx, |editor, window, cx| {
9386 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9387 s.select_display_ranges([
9388 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9389 ]);
9390 });
9391 });
9392 editor.update(cx, |editor, cx| {
9393 assert_text_with_selections(
9394 editor,
9395 indoc! {r#"
9396 let a = {
9397 key:ˇ "value",
9398 };
9399 "#},
9400 cx,
9401 );
9402 });
9403 editor.update_in(cx, |editor, window, cx| {
9404 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9405 });
9406 editor.update(cx, |editor, cx| {
9407 assert_text_with_selections(
9408 editor,
9409 indoc! {r#"
9410 let a = {
9411 «ˇkey: "value"»,
9412 };
9413 "#},
9414 cx,
9415 );
9416 });
9417 editor.update_in(cx, |editor, window, cx| {
9418 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9419 });
9420 editor.update(cx, |editor, cx| {
9421 assert_text_with_selections(
9422 editor,
9423 indoc! {r#"
9424 let a = «ˇ{
9425 key: "value",
9426 }»;
9427 "#},
9428 cx,
9429 );
9430 });
9431
9432 // Test case 3: Cursor after ','
9433 editor.update_in(cx, |editor, window, cx| {
9434 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9435 s.select_display_ranges([
9436 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9437 ]);
9438 });
9439 });
9440 editor.update(cx, |editor, cx| {
9441 assert_text_with_selections(
9442 editor,
9443 indoc! {r#"
9444 let a = {
9445 key: "value",ˇ
9446 };
9447 "#},
9448 cx,
9449 );
9450 });
9451 editor.update_in(cx, |editor, window, cx| {
9452 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9453 });
9454 editor.update(cx, |editor, cx| {
9455 assert_text_with_selections(
9456 editor,
9457 indoc! {r#"
9458 let a = «ˇ{
9459 key: "value",
9460 }»;
9461 "#},
9462 cx,
9463 );
9464 });
9465
9466 // Test case 4: Cursor after ';'
9467 editor.update_in(cx, |editor, window, cx| {
9468 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9469 s.select_display_ranges([
9470 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9471 ]);
9472 });
9473 });
9474 editor.update(cx, |editor, cx| {
9475 assert_text_with_selections(
9476 editor,
9477 indoc! {r#"
9478 let a = {
9479 key: "value",
9480 };ˇ
9481 "#},
9482 cx,
9483 );
9484 });
9485 editor.update_in(cx, |editor, window, cx| {
9486 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9487 });
9488 editor.update(cx, |editor, cx| {
9489 assert_text_with_selections(
9490 editor,
9491 indoc! {r#"
9492 «ˇlet a = {
9493 key: "value",
9494 };
9495 »"#},
9496 cx,
9497 );
9498 });
9499}
9500
9501#[gpui::test]
9502async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9503 init_test(cx, |_| {});
9504
9505 let language = Arc::new(Language::new(
9506 LanguageConfig::default(),
9507 Some(tree_sitter_rust::LANGUAGE.into()),
9508 ));
9509
9510 let text = r#"
9511 use mod1::mod2::{mod3, mod4};
9512
9513 fn fn_1(param1: bool, param2: &str) {
9514 let var1 = "hello world";
9515 }
9516 "#
9517 .unindent();
9518
9519 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9520 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9521 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9522
9523 editor
9524 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9525 .await;
9526
9527 // Test 1: Cursor on a letter of a string word
9528 editor.update_in(cx, |editor, window, cx| {
9529 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9530 s.select_display_ranges([
9531 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9532 ]);
9533 });
9534 });
9535 editor.update_in(cx, |editor, window, cx| {
9536 assert_text_with_selections(
9537 editor,
9538 indoc! {r#"
9539 use mod1::mod2::{mod3, mod4};
9540
9541 fn fn_1(param1: bool, param2: &str) {
9542 let var1 = "hˇello world";
9543 }
9544 "#},
9545 cx,
9546 );
9547 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9548 assert_text_with_selections(
9549 editor,
9550 indoc! {r#"
9551 use mod1::mod2::{mod3, mod4};
9552
9553 fn fn_1(param1: bool, param2: &str) {
9554 let var1 = "«ˇhello» world";
9555 }
9556 "#},
9557 cx,
9558 );
9559 });
9560
9561 // Test 2: Partial selection within a word
9562 editor.update_in(cx, |editor, window, cx| {
9563 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9564 s.select_display_ranges([
9565 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9566 ]);
9567 });
9568 });
9569 editor.update_in(cx, |editor, window, cx| {
9570 assert_text_with_selections(
9571 editor,
9572 indoc! {r#"
9573 use mod1::mod2::{mod3, mod4};
9574
9575 fn fn_1(param1: bool, param2: &str) {
9576 let var1 = "h«elˇ»lo world";
9577 }
9578 "#},
9579 cx,
9580 );
9581 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9582 assert_text_with_selections(
9583 editor,
9584 indoc! {r#"
9585 use mod1::mod2::{mod3, mod4};
9586
9587 fn fn_1(param1: bool, param2: &str) {
9588 let var1 = "«ˇhello» world";
9589 }
9590 "#},
9591 cx,
9592 );
9593 });
9594
9595 // Test 3: Complete word already selected
9596 editor.update_in(cx, |editor, window, cx| {
9597 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9598 s.select_display_ranges([
9599 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9600 ]);
9601 });
9602 });
9603 editor.update_in(cx, |editor, window, cx| {
9604 assert_text_with_selections(
9605 editor,
9606 indoc! {r#"
9607 use mod1::mod2::{mod3, mod4};
9608
9609 fn fn_1(param1: bool, param2: &str) {
9610 let var1 = "«helloˇ» world";
9611 }
9612 "#},
9613 cx,
9614 );
9615 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9616 assert_text_with_selections(
9617 editor,
9618 indoc! {r#"
9619 use mod1::mod2::{mod3, mod4};
9620
9621 fn fn_1(param1: bool, param2: &str) {
9622 let var1 = "«hello worldˇ»";
9623 }
9624 "#},
9625 cx,
9626 );
9627 });
9628
9629 // Test 4: Selection spanning across words
9630 editor.update_in(cx, |editor, window, cx| {
9631 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9632 s.select_display_ranges([
9633 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9634 ]);
9635 });
9636 });
9637 editor.update_in(cx, |editor, window, cx| {
9638 assert_text_with_selections(
9639 editor,
9640 indoc! {r#"
9641 use mod1::mod2::{mod3, mod4};
9642
9643 fn fn_1(param1: bool, param2: &str) {
9644 let var1 = "hel«lo woˇ»rld";
9645 }
9646 "#},
9647 cx,
9648 );
9649 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9650 assert_text_with_selections(
9651 editor,
9652 indoc! {r#"
9653 use mod1::mod2::{mod3, mod4};
9654
9655 fn fn_1(param1: bool, param2: &str) {
9656 let var1 = "«ˇhello world»";
9657 }
9658 "#},
9659 cx,
9660 );
9661 });
9662
9663 // Test 5: Expansion beyond string
9664 editor.update_in(cx, |editor, window, cx| {
9665 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9666 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9667 assert_text_with_selections(
9668 editor,
9669 indoc! {r#"
9670 use mod1::mod2::{mod3, mod4};
9671
9672 fn fn_1(param1: bool, param2: &str) {
9673 «ˇlet var1 = "hello world";»
9674 }
9675 "#},
9676 cx,
9677 );
9678 });
9679}
9680
9681#[gpui::test]
9682async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9683 init_test(cx, |_| {});
9684
9685 let mut cx = EditorTestContext::new(cx).await;
9686
9687 let language = Arc::new(Language::new(
9688 LanguageConfig::default(),
9689 Some(tree_sitter_rust::LANGUAGE.into()),
9690 ));
9691
9692 cx.update_buffer(|buffer, cx| {
9693 buffer.set_language(Some(language), cx);
9694 });
9695
9696 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9697 cx.update_editor(|editor, window, cx| {
9698 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9699 });
9700
9701 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9702
9703 cx.set_state(indoc! { r#"fn a() {
9704 // what
9705 // a
9706 // ˇlong
9707 // method
9708 // I
9709 // sure
9710 // hope
9711 // it
9712 // works
9713 }"# });
9714
9715 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9716 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9717 cx.update(|_, cx| {
9718 multi_buffer.update(cx, |multi_buffer, cx| {
9719 multi_buffer.set_excerpts_for_path(
9720 PathKey::for_buffer(&buffer, cx),
9721 buffer,
9722 [Point::new(1, 0)..Point::new(1, 0)],
9723 3,
9724 cx,
9725 );
9726 });
9727 });
9728
9729 let editor2 = cx.new_window_entity(|window, cx| {
9730 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9731 });
9732
9733 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9734 cx.update_editor(|editor, window, cx| {
9735 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9736 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9737 })
9738 });
9739
9740 cx.assert_editor_state(indoc! { "
9741 fn a() {
9742 // what
9743 // a
9744 ˇ // long
9745 // method"});
9746
9747 cx.update_editor(|editor, window, cx| {
9748 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9749 });
9750
9751 // Although we could potentially make the action work when the syntax node
9752 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9753 // did. Maybe we could also expand the excerpt to contain the range?
9754 cx.assert_editor_state(indoc! { "
9755 fn a() {
9756 // what
9757 // a
9758 ˇ // long
9759 // method"});
9760}
9761
9762#[gpui::test]
9763async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9764 init_test(cx, |_| {});
9765
9766 let base_text = r#"
9767 impl A {
9768 // this is an uncommitted comment
9769
9770 fn b() {
9771 c();
9772 }
9773
9774 // this is another uncommitted comment
9775
9776 fn d() {
9777 // e
9778 // f
9779 }
9780 }
9781
9782 fn g() {
9783 // h
9784 }
9785 "#
9786 .unindent();
9787
9788 let text = r#"
9789 ˇimpl A {
9790
9791 fn b() {
9792 c();
9793 }
9794
9795 fn d() {
9796 // e
9797 // f
9798 }
9799 }
9800
9801 fn g() {
9802 // h
9803 }
9804 "#
9805 .unindent();
9806
9807 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9808 cx.set_state(&text);
9809 cx.set_head_text(&base_text);
9810 cx.update_editor(|editor, window, cx| {
9811 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9812 });
9813
9814 cx.assert_state_with_diff(
9815 "
9816 ˇimpl A {
9817 - // this is an uncommitted comment
9818
9819 fn b() {
9820 c();
9821 }
9822
9823 - // this is another uncommitted comment
9824 -
9825 fn d() {
9826 // e
9827 // f
9828 }
9829 }
9830
9831 fn g() {
9832 // h
9833 }
9834 "
9835 .unindent(),
9836 );
9837
9838 let expected_display_text = "
9839 impl A {
9840 // this is an uncommitted comment
9841
9842 fn b() {
9843 ⋯
9844 }
9845
9846 // this is another uncommitted comment
9847
9848 fn d() {
9849 ⋯
9850 }
9851 }
9852
9853 fn g() {
9854 ⋯
9855 }
9856 "
9857 .unindent();
9858
9859 cx.update_editor(|editor, window, cx| {
9860 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9861 assert_eq!(editor.display_text(cx), expected_display_text);
9862 });
9863}
9864
9865#[gpui::test]
9866async fn test_autoindent(cx: &mut TestAppContext) {
9867 init_test(cx, |_| {});
9868
9869 let language = Arc::new(
9870 Language::new(
9871 LanguageConfig {
9872 brackets: BracketPairConfig {
9873 pairs: vec![
9874 BracketPair {
9875 start: "{".to_string(),
9876 end: "}".to_string(),
9877 close: false,
9878 surround: false,
9879 newline: true,
9880 },
9881 BracketPair {
9882 start: "(".to_string(),
9883 end: ")".to_string(),
9884 close: false,
9885 surround: false,
9886 newline: true,
9887 },
9888 ],
9889 ..Default::default()
9890 },
9891 ..Default::default()
9892 },
9893 Some(tree_sitter_rust::LANGUAGE.into()),
9894 )
9895 .with_indents_query(
9896 r#"
9897 (_ "(" ")" @end) @indent
9898 (_ "{" "}" @end) @indent
9899 "#,
9900 )
9901 .unwrap(),
9902 );
9903
9904 let text = "fn a() {}";
9905
9906 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9907 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9908 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9909 editor
9910 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9911 .await;
9912
9913 editor.update_in(cx, |editor, window, cx| {
9914 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9915 s.select_ranges([
9916 MultiBufferOffset(5)..MultiBufferOffset(5),
9917 MultiBufferOffset(8)..MultiBufferOffset(8),
9918 MultiBufferOffset(9)..MultiBufferOffset(9),
9919 ])
9920 });
9921 editor.newline(&Newline, window, cx);
9922 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9923 assert_eq!(
9924 editor.selections.ranges(&editor.display_snapshot(cx)),
9925 &[
9926 Point::new(1, 4)..Point::new(1, 4),
9927 Point::new(3, 4)..Point::new(3, 4),
9928 Point::new(5, 0)..Point::new(5, 0)
9929 ]
9930 );
9931 });
9932}
9933
9934#[gpui::test]
9935async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9936 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9937
9938 let language = Arc::new(
9939 Language::new(
9940 LanguageConfig {
9941 brackets: BracketPairConfig {
9942 pairs: vec![
9943 BracketPair {
9944 start: "{".to_string(),
9945 end: "}".to_string(),
9946 close: false,
9947 surround: false,
9948 newline: true,
9949 },
9950 BracketPair {
9951 start: "(".to_string(),
9952 end: ")".to_string(),
9953 close: false,
9954 surround: false,
9955 newline: true,
9956 },
9957 ],
9958 ..Default::default()
9959 },
9960 ..Default::default()
9961 },
9962 Some(tree_sitter_rust::LANGUAGE.into()),
9963 )
9964 .with_indents_query(
9965 r#"
9966 (_ "(" ")" @end) @indent
9967 (_ "{" "}" @end) @indent
9968 "#,
9969 )
9970 .unwrap(),
9971 );
9972
9973 let text = "fn a() {}";
9974
9975 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9976 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9977 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9978 editor
9979 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9980 .await;
9981
9982 editor.update_in(cx, |editor, window, cx| {
9983 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9984 s.select_ranges([
9985 MultiBufferOffset(5)..MultiBufferOffset(5),
9986 MultiBufferOffset(8)..MultiBufferOffset(8),
9987 MultiBufferOffset(9)..MultiBufferOffset(9),
9988 ])
9989 });
9990 editor.newline(&Newline, window, cx);
9991 assert_eq!(
9992 editor.text(cx),
9993 indoc!(
9994 "
9995 fn a(
9996
9997 ) {
9998
9999 }
10000 "
10001 )
10002 );
10003 assert_eq!(
10004 editor.selections.ranges(&editor.display_snapshot(cx)),
10005 &[
10006 Point::new(1, 0)..Point::new(1, 0),
10007 Point::new(3, 0)..Point::new(3, 0),
10008 Point::new(5, 0)..Point::new(5, 0)
10009 ]
10010 );
10011 });
10012}
10013
10014#[gpui::test]
10015async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
10016 init_test(cx, |settings| {
10017 settings.defaults.auto_indent = Some(true);
10018 settings.languages.0.insert(
10019 "python".into(),
10020 LanguageSettingsContent {
10021 auto_indent: Some(false),
10022 ..Default::default()
10023 },
10024 );
10025 });
10026
10027 let mut cx = EditorTestContext::new(cx).await;
10028
10029 let injected_language = Arc::new(
10030 Language::new(
10031 LanguageConfig {
10032 brackets: BracketPairConfig {
10033 pairs: vec![
10034 BracketPair {
10035 start: "{".to_string(),
10036 end: "}".to_string(),
10037 close: false,
10038 surround: false,
10039 newline: true,
10040 },
10041 BracketPair {
10042 start: "(".to_string(),
10043 end: ")".to_string(),
10044 close: true,
10045 surround: false,
10046 newline: true,
10047 },
10048 ],
10049 ..Default::default()
10050 },
10051 name: "python".into(),
10052 ..Default::default()
10053 },
10054 Some(tree_sitter_python::LANGUAGE.into()),
10055 )
10056 .with_indents_query(
10057 r#"
10058 (_ "(" ")" @end) @indent
10059 (_ "{" "}" @end) @indent
10060 "#,
10061 )
10062 .unwrap(),
10063 );
10064
10065 let language = Arc::new(
10066 Language::new(
10067 LanguageConfig {
10068 brackets: BracketPairConfig {
10069 pairs: vec![
10070 BracketPair {
10071 start: "{".to_string(),
10072 end: "}".to_string(),
10073 close: false,
10074 surround: false,
10075 newline: true,
10076 },
10077 BracketPair {
10078 start: "(".to_string(),
10079 end: ")".to_string(),
10080 close: true,
10081 surround: false,
10082 newline: true,
10083 },
10084 ],
10085 ..Default::default()
10086 },
10087 name: LanguageName::new_static("rust"),
10088 ..Default::default()
10089 },
10090 Some(tree_sitter_rust::LANGUAGE.into()),
10091 )
10092 .with_indents_query(
10093 r#"
10094 (_ "(" ")" @end) @indent
10095 (_ "{" "}" @end) @indent
10096 "#,
10097 )
10098 .unwrap()
10099 .with_injection_query(
10100 r#"
10101 (macro_invocation
10102 macro: (identifier) @_macro_name
10103 (token_tree) @injection.content
10104 (#set! injection.language "python"))
10105 "#,
10106 )
10107 .unwrap(),
10108 );
10109
10110 cx.language_registry().add(injected_language);
10111 cx.language_registry().add(language.clone());
10112
10113 cx.update_buffer(|buffer, cx| {
10114 buffer.set_language(Some(language), cx);
10115 });
10116
10117 cx.set_state(r#"struct A {ˇ}"#);
10118
10119 cx.update_editor(|editor, window, cx| {
10120 editor.newline(&Default::default(), window, cx);
10121 });
10122
10123 cx.assert_editor_state(indoc!(
10124 "struct A {
10125 ˇ
10126 }"
10127 ));
10128
10129 cx.set_state(r#"select_biased!(ˇ)"#);
10130
10131 cx.update_editor(|editor, window, cx| {
10132 editor.newline(&Default::default(), window, cx);
10133 editor.handle_input("def ", window, cx);
10134 editor.handle_input("(", window, cx);
10135 editor.newline(&Default::default(), window, cx);
10136 editor.handle_input("a", window, cx);
10137 });
10138
10139 cx.assert_editor_state(indoc!(
10140 "select_biased!(
10141 def (
10142 aˇ
10143 )
10144 )"
10145 ));
10146}
10147
10148#[gpui::test]
10149async fn test_autoindent_selections(cx: &mut TestAppContext) {
10150 init_test(cx, |_| {});
10151
10152 {
10153 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
10154 cx.set_state(indoc! {"
10155 impl A {
10156
10157 fn b() {}
10158
10159 «fn c() {
10160
10161 }ˇ»
10162 }
10163 "});
10164
10165 cx.update_editor(|editor, window, cx| {
10166 editor.autoindent(&Default::default(), window, cx);
10167 });
10168
10169 cx.assert_editor_state(indoc! {"
10170 impl A {
10171
10172 fn b() {}
10173
10174 «fn c() {
10175
10176 }ˇ»
10177 }
10178 "});
10179 }
10180
10181 {
10182 let mut cx = EditorTestContext::new_multibuffer(
10183 cx,
10184 [indoc! { "
10185 impl A {
10186 «
10187 // a
10188 fn b(){}
10189 »
10190 «
10191 }
10192 fn c(){}
10193 »
10194 "}],
10195 );
10196
10197 let buffer = cx.update_editor(|editor, _, cx| {
10198 let buffer = editor.buffer().update(cx, |buffer, _| {
10199 buffer.all_buffers().iter().next().unwrap().clone()
10200 });
10201 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
10202 buffer
10203 });
10204
10205 cx.run_until_parked();
10206 cx.update_editor(|editor, window, cx| {
10207 editor.select_all(&Default::default(), window, cx);
10208 editor.autoindent(&Default::default(), window, cx)
10209 });
10210 cx.run_until_parked();
10211
10212 cx.update(|_, cx| {
10213 assert_eq!(
10214 buffer.read(cx).text(),
10215 indoc! { "
10216 impl A {
10217
10218 // a
10219 fn b(){}
10220
10221
10222 }
10223 fn c(){}
10224
10225 " }
10226 )
10227 });
10228 }
10229}
10230
10231#[gpui::test]
10232async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
10233 init_test(cx, |_| {});
10234
10235 let mut cx = EditorTestContext::new(cx).await;
10236
10237 let language = Arc::new(Language::new(
10238 LanguageConfig {
10239 brackets: BracketPairConfig {
10240 pairs: vec![
10241 BracketPair {
10242 start: "{".to_string(),
10243 end: "}".to_string(),
10244 close: true,
10245 surround: true,
10246 newline: true,
10247 },
10248 BracketPair {
10249 start: "(".to_string(),
10250 end: ")".to_string(),
10251 close: true,
10252 surround: true,
10253 newline: true,
10254 },
10255 BracketPair {
10256 start: "/*".to_string(),
10257 end: " */".to_string(),
10258 close: true,
10259 surround: true,
10260 newline: true,
10261 },
10262 BracketPair {
10263 start: "[".to_string(),
10264 end: "]".to_string(),
10265 close: false,
10266 surround: false,
10267 newline: true,
10268 },
10269 BracketPair {
10270 start: "\"".to_string(),
10271 end: "\"".to_string(),
10272 close: true,
10273 surround: true,
10274 newline: false,
10275 },
10276 BracketPair {
10277 start: "<".to_string(),
10278 end: ">".to_string(),
10279 close: false,
10280 surround: true,
10281 newline: true,
10282 },
10283 ],
10284 ..Default::default()
10285 },
10286 autoclose_before: "})]".to_string(),
10287 ..Default::default()
10288 },
10289 Some(tree_sitter_rust::LANGUAGE.into()),
10290 ));
10291
10292 cx.language_registry().add(language.clone());
10293 cx.update_buffer(|buffer, cx| {
10294 buffer.set_language(Some(language), cx);
10295 });
10296
10297 cx.set_state(
10298 &r#"
10299 🏀ˇ
10300 εˇ
10301 ❤️ˇ
10302 "#
10303 .unindent(),
10304 );
10305
10306 // autoclose multiple nested brackets at multiple cursors
10307 cx.update_editor(|editor, window, cx| {
10308 editor.handle_input("{", window, cx);
10309 editor.handle_input("{", window, cx);
10310 editor.handle_input("{", window, cx);
10311 });
10312 cx.assert_editor_state(
10313 &"
10314 🏀{{{ˇ}}}
10315 ε{{{ˇ}}}
10316 ❤️{{{ˇ}}}
10317 "
10318 .unindent(),
10319 );
10320
10321 // insert a different closing bracket
10322 cx.update_editor(|editor, window, cx| {
10323 editor.handle_input(")", window, cx);
10324 });
10325 cx.assert_editor_state(
10326 &"
10327 🏀{{{)ˇ}}}
10328 ε{{{)ˇ}}}
10329 ❤️{{{)ˇ}}}
10330 "
10331 .unindent(),
10332 );
10333
10334 // skip over the auto-closed brackets when typing a closing bracket
10335 cx.update_editor(|editor, window, cx| {
10336 editor.move_right(&MoveRight, window, cx);
10337 editor.handle_input("}", window, cx);
10338 editor.handle_input("}", window, cx);
10339 editor.handle_input("}", window, cx);
10340 });
10341 cx.assert_editor_state(
10342 &"
10343 🏀{{{)}}}}ˇ
10344 ε{{{)}}}}ˇ
10345 ❤️{{{)}}}}ˇ
10346 "
10347 .unindent(),
10348 );
10349
10350 // autoclose multi-character pairs
10351 cx.set_state(
10352 &"
10353 ˇ
10354 ˇ
10355 "
10356 .unindent(),
10357 );
10358 cx.update_editor(|editor, window, cx| {
10359 editor.handle_input("/", window, cx);
10360 editor.handle_input("*", window, cx);
10361 });
10362 cx.assert_editor_state(
10363 &"
10364 /*ˇ */
10365 /*ˇ */
10366 "
10367 .unindent(),
10368 );
10369
10370 // one cursor autocloses a multi-character pair, one cursor
10371 // does not autoclose.
10372 cx.set_state(
10373 &"
10374 /ˇ
10375 ˇ
10376 "
10377 .unindent(),
10378 );
10379 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10380 cx.assert_editor_state(
10381 &"
10382 /*ˇ */
10383 *ˇ
10384 "
10385 .unindent(),
10386 );
10387
10388 // Don't autoclose if the next character isn't whitespace and isn't
10389 // listed in the language's "autoclose_before" section.
10390 cx.set_state("ˇa b");
10391 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10392 cx.assert_editor_state("{ˇa b");
10393
10394 // Don't autoclose if `close` is false for the bracket pair
10395 cx.set_state("ˇ");
10396 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10397 cx.assert_editor_state("[ˇ");
10398
10399 // Surround with brackets if text is selected
10400 cx.set_state("«aˇ» b");
10401 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10402 cx.assert_editor_state("{«aˇ»} b");
10403
10404 // Autoclose when not immediately after a word character
10405 cx.set_state("a ˇ");
10406 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10407 cx.assert_editor_state("a \"ˇ\"");
10408
10409 // Autoclose pair where the start and end characters are the same
10410 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10411 cx.assert_editor_state("a \"\"ˇ");
10412
10413 // Don't autoclose when immediately after a word character
10414 cx.set_state("aˇ");
10415 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10416 cx.assert_editor_state("a\"ˇ");
10417
10418 // Do autoclose when after a non-word character
10419 cx.set_state("{ˇ");
10420 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10421 cx.assert_editor_state("{\"ˇ\"");
10422
10423 // Non identical pairs autoclose regardless of preceding character
10424 cx.set_state("aˇ");
10425 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10426 cx.assert_editor_state("a{ˇ}");
10427
10428 // Don't autoclose pair if autoclose is disabled
10429 cx.set_state("ˇ");
10430 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10431 cx.assert_editor_state("<ˇ");
10432
10433 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10434 cx.set_state("«aˇ» b");
10435 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10436 cx.assert_editor_state("<«aˇ»> b");
10437}
10438
10439#[gpui::test]
10440async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10441 init_test(cx, |settings| {
10442 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10443 });
10444
10445 let mut cx = EditorTestContext::new(cx).await;
10446
10447 let language = Arc::new(Language::new(
10448 LanguageConfig {
10449 brackets: BracketPairConfig {
10450 pairs: vec![
10451 BracketPair {
10452 start: "{".to_string(),
10453 end: "}".to_string(),
10454 close: true,
10455 surround: true,
10456 newline: true,
10457 },
10458 BracketPair {
10459 start: "(".to_string(),
10460 end: ")".to_string(),
10461 close: true,
10462 surround: true,
10463 newline: true,
10464 },
10465 BracketPair {
10466 start: "[".to_string(),
10467 end: "]".to_string(),
10468 close: false,
10469 surround: false,
10470 newline: true,
10471 },
10472 ],
10473 ..Default::default()
10474 },
10475 autoclose_before: "})]".to_string(),
10476 ..Default::default()
10477 },
10478 Some(tree_sitter_rust::LANGUAGE.into()),
10479 ));
10480
10481 cx.language_registry().add(language.clone());
10482 cx.update_buffer(|buffer, cx| {
10483 buffer.set_language(Some(language), cx);
10484 });
10485
10486 cx.set_state(
10487 &"
10488 ˇ
10489 ˇ
10490 ˇ
10491 "
10492 .unindent(),
10493 );
10494
10495 // ensure only matching closing brackets are skipped over
10496 cx.update_editor(|editor, window, cx| {
10497 editor.handle_input("}", window, cx);
10498 editor.move_left(&MoveLeft, window, cx);
10499 editor.handle_input(")", window, cx);
10500 editor.move_left(&MoveLeft, window, cx);
10501 });
10502 cx.assert_editor_state(
10503 &"
10504 ˇ)}
10505 ˇ)}
10506 ˇ)}
10507 "
10508 .unindent(),
10509 );
10510
10511 // skip-over closing brackets at multiple cursors
10512 cx.update_editor(|editor, window, cx| {
10513 editor.handle_input(")", window, cx);
10514 editor.handle_input("}", window, cx);
10515 });
10516 cx.assert_editor_state(
10517 &"
10518 )}ˇ
10519 )}ˇ
10520 )}ˇ
10521 "
10522 .unindent(),
10523 );
10524
10525 // ignore non-close brackets
10526 cx.update_editor(|editor, window, cx| {
10527 editor.handle_input("]", window, cx);
10528 editor.move_left(&MoveLeft, window, cx);
10529 editor.handle_input("]", window, cx);
10530 });
10531 cx.assert_editor_state(
10532 &"
10533 )}]ˇ]
10534 )}]ˇ]
10535 )}]ˇ]
10536 "
10537 .unindent(),
10538 );
10539}
10540
10541#[gpui::test]
10542async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10543 init_test(cx, |_| {});
10544
10545 let mut cx = EditorTestContext::new(cx).await;
10546
10547 let html_language = Arc::new(
10548 Language::new(
10549 LanguageConfig {
10550 name: "HTML".into(),
10551 brackets: BracketPairConfig {
10552 pairs: vec![
10553 BracketPair {
10554 start: "<".into(),
10555 end: ">".into(),
10556 close: true,
10557 ..Default::default()
10558 },
10559 BracketPair {
10560 start: "{".into(),
10561 end: "}".into(),
10562 close: true,
10563 ..Default::default()
10564 },
10565 BracketPair {
10566 start: "(".into(),
10567 end: ")".into(),
10568 close: true,
10569 ..Default::default()
10570 },
10571 ],
10572 ..Default::default()
10573 },
10574 autoclose_before: "})]>".into(),
10575 ..Default::default()
10576 },
10577 Some(tree_sitter_html::LANGUAGE.into()),
10578 )
10579 .with_injection_query(
10580 r#"
10581 (script_element
10582 (raw_text) @injection.content
10583 (#set! injection.language "javascript"))
10584 "#,
10585 )
10586 .unwrap(),
10587 );
10588
10589 let javascript_language = Arc::new(Language::new(
10590 LanguageConfig {
10591 name: "JavaScript".into(),
10592 brackets: BracketPairConfig {
10593 pairs: vec![
10594 BracketPair {
10595 start: "/*".into(),
10596 end: " */".into(),
10597 close: true,
10598 ..Default::default()
10599 },
10600 BracketPair {
10601 start: "{".into(),
10602 end: "}".into(),
10603 close: true,
10604 ..Default::default()
10605 },
10606 BracketPair {
10607 start: "(".into(),
10608 end: ")".into(),
10609 close: true,
10610 ..Default::default()
10611 },
10612 ],
10613 ..Default::default()
10614 },
10615 autoclose_before: "})]>".into(),
10616 ..Default::default()
10617 },
10618 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10619 ));
10620
10621 cx.language_registry().add(html_language.clone());
10622 cx.language_registry().add(javascript_language);
10623 cx.executor().run_until_parked();
10624
10625 cx.update_buffer(|buffer, cx| {
10626 buffer.set_language(Some(html_language), cx);
10627 });
10628
10629 cx.set_state(
10630 &r#"
10631 <body>ˇ
10632 <script>
10633 var x = 1;ˇ
10634 </script>
10635 </body>ˇ
10636 "#
10637 .unindent(),
10638 );
10639
10640 // Precondition: different languages are active at different locations.
10641 cx.update_editor(|editor, window, cx| {
10642 let snapshot = editor.snapshot(window, cx);
10643 let cursors = editor
10644 .selections
10645 .ranges::<MultiBufferOffset>(&editor.display_snapshot(cx));
10646 let languages = cursors
10647 .iter()
10648 .map(|c| snapshot.language_at(c.start).unwrap().name())
10649 .collect::<Vec<_>>();
10650 assert_eq!(
10651 languages,
10652 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10653 );
10654 });
10655
10656 // Angle brackets autoclose in HTML, but not JavaScript.
10657 cx.update_editor(|editor, window, cx| {
10658 editor.handle_input("<", window, cx);
10659 editor.handle_input("a", window, cx);
10660 });
10661 cx.assert_editor_state(
10662 &r#"
10663 <body><aˇ>
10664 <script>
10665 var x = 1;<aˇ
10666 </script>
10667 </body><aˇ>
10668 "#
10669 .unindent(),
10670 );
10671
10672 // Curly braces and parens autoclose in both HTML and JavaScript.
10673 cx.update_editor(|editor, window, cx| {
10674 editor.handle_input(" b=", window, cx);
10675 editor.handle_input("{", window, cx);
10676 editor.handle_input("c", window, cx);
10677 editor.handle_input("(", window, cx);
10678 });
10679 cx.assert_editor_state(
10680 &r#"
10681 <body><a b={c(ˇ)}>
10682 <script>
10683 var x = 1;<a b={c(ˇ)}
10684 </script>
10685 </body><a b={c(ˇ)}>
10686 "#
10687 .unindent(),
10688 );
10689
10690 // Brackets that were already autoclosed are skipped.
10691 cx.update_editor(|editor, window, cx| {
10692 editor.handle_input(")", window, cx);
10693 editor.handle_input("d", window, cx);
10694 editor.handle_input("}", window, cx);
10695 });
10696 cx.assert_editor_state(
10697 &r#"
10698 <body><a b={c()d}ˇ>
10699 <script>
10700 var x = 1;<a b={c()d}ˇ
10701 </script>
10702 </body><a b={c()d}ˇ>
10703 "#
10704 .unindent(),
10705 );
10706 cx.update_editor(|editor, window, cx| {
10707 editor.handle_input(">", window, cx);
10708 });
10709 cx.assert_editor_state(
10710 &r#"
10711 <body><a b={c()d}>ˇ
10712 <script>
10713 var x = 1;<a b={c()d}>ˇ
10714 </script>
10715 </body><a b={c()d}>ˇ
10716 "#
10717 .unindent(),
10718 );
10719
10720 // Reset
10721 cx.set_state(
10722 &r#"
10723 <body>ˇ
10724 <script>
10725 var x = 1;ˇ
10726 </script>
10727 </body>ˇ
10728 "#
10729 .unindent(),
10730 );
10731
10732 cx.update_editor(|editor, window, cx| {
10733 editor.handle_input("<", window, cx);
10734 });
10735 cx.assert_editor_state(
10736 &r#"
10737 <body><ˇ>
10738 <script>
10739 var x = 1;<ˇ
10740 </script>
10741 </body><ˇ>
10742 "#
10743 .unindent(),
10744 );
10745
10746 // When backspacing, the closing angle brackets are removed.
10747 cx.update_editor(|editor, window, cx| {
10748 editor.backspace(&Backspace, window, cx);
10749 });
10750 cx.assert_editor_state(
10751 &r#"
10752 <body>ˇ
10753 <script>
10754 var x = 1;ˇ
10755 </script>
10756 </body>ˇ
10757 "#
10758 .unindent(),
10759 );
10760
10761 // Block comments autoclose in JavaScript, but not HTML.
10762 cx.update_editor(|editor, window, cx| {
10763 editor.handle_input("/", window, cx);
10764 editor.handle_input("*", window, cx);
10765 });
10766 cx.assert_editor_state(
10767 &r#"
10768 <body>/*ˇ
10769 <script>
10770 var x = 1;/*ˇ */
10771 </script>
10772 </body>/*ˇ
10773 "#
10774 .unindent(),
10775 );
10776}
10777
10778#[gpui::test]
10779async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10780 init_test(cx, |_| {});
10781
10782 let mut cx = EditorTestContext::new(cx).await;
10783
10784 let rust_language = Arc::new(
10785 Language::new(
10786 LanguageConfig {
10787 name: "Rust".into(),
10788 brackets: serde_json::from_value(json!([
10789 { "start": "{", "end": "}", "close": true, "newline": true },
10790 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10791 ]))
10792 .unwrap(),
10793 autoclose_before: "})]>".into(),
10794 ..Default::default()
10795 },
10796 Some(tree_sitter_rust::LANGUAGE.into()),
10797 )
10798 .with_override_query("(string_literal) @string")
10799 .unwrap(),
10800 );
10801
10802 cx.language_registry().add(rust_language.clone());
10803 cx.update_buffer(|buffer, cx| {
10804 buffer.set_language(Some(rust_language), cx);
10805 });
10806
10807 cx.set_state(
10808 &r#"
10809 let x = ˇ
10810 "#
10811 .unindent(),
10812 );
10813
10814 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10815 cx.update_editor(|editor, window, cx| {
10816 editor.handle_input("\"", window, cx);
10817 });
10818 cx.assert_editor_state(
10819 &r#"
10820 let x = "ˇ"
10821 "#
10822 .unindent(),
10823 );
10824
10825 // Inserting another quotation mark. The cursor moves across the existing
10826 // automatically-inserted quotation mark.
10827 cx.update_editor(|editor, window, cx| {
10828 editor.handle_input("\"", window, cx);
10829 });
10830 cx.assert_editor_state(
10831 &r#"
10832 let x = ""ˇ
10833 "#
10834 .unindent(),
10835 );
10836
10837 // Reset
10838 cx.set_state(
10839 &r#"
10840 let x = ˇ
10841 "#
10842 .unindent(),
10843 );
10844
10845 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10846 cx.update_editor(|editor, window, cx| {
10847 editor.handle_input("\"", window, cx);
10848 editor.handle_input(" ", window, cx);
10849 editor.move_left(&Default::default(), window, cx);
10850 editor.handle_input("\\", window, cx);
10851 editor.handle_input("\"", window, cx);
10852 });
10853 cx.assert_editor_state(
10854 &r#"
10855 let x = "\"ˇ "
10856 "#
10857 .unindent(),
10858 );
10859
10860 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10861 // mark. Nothing is inserted.
10862 cx.update_editor(|editor, window, cx| {
10863 editor.move_right(&Default::default(), window, cx);
10864 editor.handle_input("\"", window, cx);
10865 });
10866 cx.assert_editor_state(
10867 &r#"
10868 let x = "\" "ˇ
10869 "#
10870 .unindent(),
10871 );
10872}
10873
10874#[gpui::test]
10875async fn test_autoclose_quotes_with_scope_awareness(cx: &mut TestAppContext) {
10876 init_test(cx, |_| {});
10877
10878 let mut cx = EditorTestContext::new(cx).await;
10879 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
10880
10881 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10882
10883 // Double quote inside single-quoted string
10884 cx.set_state(indoc! {r#"
10885 def main():
10886 items = ['"', ˇ]
10887 "#});
10888 cx.update_editor(|editor, window, cx| {
10889 editor.handle_input("\"", window, cx);
10890 });
10891 cx.assert_editor_state(indoc! {r#"
10892 def main():
10893 items = ['"', "ˇ"]
10894 "#});
10895
10896 // Two double quotes inside single-quoted string
10897 cx.set_state(indoc! {r#"
10898 def main():
10899 items = ['""', ˇ]
10900 "#});
10901 cx.update_editor(|editor, window, cx| {
10902 editor.handle_input("\"", window, cx);
10903 });
10904 cx.assert_editor_state(indoc! {r#"
10905 def main():
10906 items = ['""', "ˇ"]
10907 "#});
10908
10909 // Single quote inside double-quoted string
10910 cx.set_state(indoc! {r#"
10911 def main():
10912 items = ["'", ˇ]
10913 "#});
10914 cx.update_editor(|editor, window, cx| {
10915 editor.handle_input("'", window, cx);
10916 });
10917 cx.assert_editor_state(indoc! {r#"
10918 def main():
10919 items = ["'", 'ˇ']
10920 "#});
10921
10922 // Two single quotes inside double-quoted string
10923 cx.set_state(indoc! {r#"
10924 def main():
10925 items = ["''", ˇ]
10926 "#});
10927 cx.update_editor(|editor, window, cx| {
10928 editor.handle_input("'", window, cx);
10929 });
10930 cx.assert_editor_state(indoc! {r#"
10931 def main():
10932 items = ["''", 'ˇ']
10933 "#});
10934
10935 // Mixed quotes on same line
10936 cx.set_state(indoc! {r#"
10937 def main():
10938 items = ['"""', "'''''", ˇ]
10939 "#});
10940 cx.update_editor(|editor, window, cx| {
10941 editor.handle_input("\"", window, cx);
10942 });
10943 cx.assert_editor_state(indoc! {r#"
10944 def main():
10945 items = ['"""', "'''''", "ˇ"]
10946 "#});
10947 cx.update_editor(|editor, window, cx| {
10948 editor.move_right(&MoveRight, window, cx);
10949 });
10950 cx.update_editor(|editor, window, cx| {
10951 editor.handle_input(", ", window, cx);
10952 });
10953 cx.update_editor(|editor, window, cx| {
10954 editor.handle_input("'", window, cx);
10955 });
10956 cx.assert_editor_state(indoc! {r#"
10957 def main():
10958 items = ['"""', "'''''", "", 'ˇ']
10959 "#});
10960}
10961
10962#[gpui::test]
10963async fn test_autoclose_quotes_with_multibyte_characters(cx: &mut TestAppContext) {
10964 init_test(cx, |_| {});
10965
10966 let mut cx = EditorTestContext::new(cx).await;
10967 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
10968 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10969
10970 cx.set_state(indoc! {r#"
10971 def main():
10972 items = ["🎉", ˇ]
10973 "#});
10974 cx.update_editor(|editor, window, cx| {
10975 editor.handle_input("\"", window, cx);
10976 });
10977 cx.assert_editor_state(indoc! {r#"
10978 def main():
10979 items = ["🎉", "ˇ"]
10980 "#});
10981}
10982
10983#[gpui::test]
10984async fn test_surround_with_pair(cx: &mut TestAppContext) {
10985 init_test(cx, |_| {});
10986
10987 let language = Arc::new(Language::new(
10988 LanguageConfig {
10989 brackets: BracketPairConfig {
10990 pairs: vec![
10991 BracketPair {
10992 start: "{".to_string(),
10993 end: "}".to_string(),
10994 close: true,
10995 surround: true,
10996 newline: true,
10997 },
10998 BracketPair {
10999 start: "/* ".to_string(),
11000 end: "*/".to_string(),
11001 close: true,
11002 surround: true,
11003 ..Default::default()
11004 },
11005 ],
11006 ..Default::default()
11007 },
11008 ..Default::default()
11009 },
11010 Some(tree_sitter_rust::LANGUAGE.into()),
11011 ));
11012
11013 let text = r#"
11014 a
11015 b
11016 c
11017 "#
11018 .unindent();
11019
11020 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11021 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11022 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11023 editor
11024 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11025 .await;
11026
11027 editor.update_in(cx, |editor, window, cx| {
11028 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11029 s.select_display_ranges([
11030 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11031 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11032 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
11033 ])
11034 });
11035
11036 editor.handle_input("{", window, cx);
11037 editor.handle_input("{", window, cx);
11038 editor.handle_input("{", window, cx);
11039 assert_eq!(
11040 editor.text(cx),
11041 "
11042 {{{a}}}
11043 {{{b}}}
11044 {{{c}}}
11045 "
11046 .unindent()
11047 );
11048 assert_eq!(
11049 display_ranges(editor, cx),
11050 [
11051 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
11052 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
11053 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
11054 ]
11055 );
11056
11057 editor.undo(&Undo, window, cx);
11058 editor.undo(&Undo, window, cx);
11059 editor.undo(&Undo, window, cx);
11060 assert_eq!(
11061 editor.text(cx),
11062 "
11063 a
11064 b
11065 c
11066 "
11067 .unindent()
11068 );
11069 assert_eq!(
11070 display_ranges(editor, cx),
11071 [
11072 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11073 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11074 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
11075 ]
11076 );
11077
11078 // Ensure inserting the first character of a multi-byte bracket pair
11079 // doesn't surround the selections with the bracket.
11080 editor.handle_input("/", window, cx);
11081 assert_eq!(
11082 editor.text(cx),
11083 "
11084 /
11085 /
11086 /
11087 "
11088 .unindent()
11089 );
11090 assert_eq!(
11091 display_ranges(editor, cx),
11092 [
11093 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
11094 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
11095 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
11096 ]
11097 );
11098
11099 editor.undo(&Undo, window, cx);
11100 assert_eq!(
11101 editor.text(cx),
11102 "
11103 a
11104 b
11105 c
11106 "
11107 .unindent()
11108 );
11109 assert_eq!(
11110 display_ranges(editor, cx),
11111 [
11112 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11113 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11114 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
11115 ]
11116 );
11117
11118 // Ensure inserting the last character of a multi-byte bracket pair
11119 // doesn't surround the selections with the bracket.
11120 editor.handle_input("*", window, cx);
11121 assert_eq!(
11122 editor.text(cx),
11123 "
11124 *
11125 *
11126 *
11127 "
11128 .unindent()
11129 );
11130 assert_eq!(
11131 display_ranges(editor, cx),
11132 [
11133 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
11134 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
11135 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
11136 ]
11137 );
11138 });
11139}
11140
11141#[gpui::test]
11142async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
11143 init_test(cx, |_| {});
11144
11145 let language = Arc::new(Language::new(
11146 LanguageConfig {
11147 brackets: BracketPairConfig {
11148 pairs: vec![BracketPair {
11149 start: "{".to_string(),
11150 end: "}".to_string(),
11151 close: true,
11152 surround: true,
11153 newline: true,
11154 }],
11155 ..Default::default()
11156 },
11157 autoclose_before: "}".to_string(),
11158 ..Default::default()
11159 },
11160 Some(tree_sitter_rust::LANGUAGE.into()),
11161 ));
11162
11163 let text = r#"
11164 a
11165 b
11166 c
11167 "#
11168 .unindent();
11169
11170 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11171 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11172 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11173 editor
11174 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11175 .await;
11176
11177 editor.update_in(cx, |editor, window, cx| {
11178 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11179 s.select_ranges([
11180 Point::new(0, 1)..Point::new(0, 1),
11181 Point::new(1, 1)..Point::new(1, 1),
11182 Point::new(2, 1)..Point::new(2, 1),
11183 ])
11184 });
11185
11186 editor.handle_input("{", window, cx);
11187 editor.handle_input("{", window, cx);
11188 editor.handle_input("_", window, cx);
11189 assert_eq!(
11190 editor.text(cx),
11191 "
11192 a{{_}}
11193 b{{_}}
11194 c{{_}}
11195 "
11196 .unindent()
11197 );
11198 assert_eq!(
11199 editor
11200 .selections
11201 .ranges::<Point>(&editor.display_snapshot(cx)),
11202 [
11203 Point::new(0, 4)..Point::new(0, 4),
11204 Point::new(1, 4)..Point::new(1, 4),
11205 Point::new(2, 4)..Point::new(2, 4)
11206 ]
11207 );
11208
11209 editor.backspace(&Default::default(), window, cx);
11210 editor.backspace(&Default::default(), window, cx);
11211 assert_eq!(
11212 editor.text(cx),
11213 "
11214 a{}
11215 b{}
11216 c{}
11217 "
11218 .unindent()
11219 );
11220 assert_eq!(
11221 editor
11222 .selections
11223 .ranges::<Point>(&editor.display_snapshot(cx)),
11224 [
11225 Point::new(0, 2)..Point::new(0, 2),
11226 Point::new(1, 2)..Point::new(1, 2),
11227 Point::new(2, 2)..Point::new(2, 2)
11228 ]
11229 );
11230
11231 editor.delete_to_previous_word_start(&Default::default(), window, cx);
11232 assert_eq!(
11233 editor.text(cx),
11234 "
11235 a
11236 b
11237 c
11238 "
11239 .unindent()
11240 );
11241 assert_eq!(
11242 editor
11243 .selections
11244 .ranges::<Point>(&editor.display_snapshot(cx)),
11245 [
11246 Point::new(0, 1)..Point::new(0, 1),
11247 Point::new(1, 1)..Point::new(1, 1),
11248 Point::new(2, 1)..Point::new(2, 1)
11249 ]
11250 );
11251 });
11252}
11253
11254#[gpui::test]
11255async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
11256 init_test(cx, |settings| {
11257 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
11258 });
11259
11260 let mut cx = EditorTestContext::new(cx).await;
11261
11262 let language = Arc::new(Language::new(
11263 LanguageConfig {
11264 brackets: BracketPairConfig {
11265 pairs: vec![
11266 BracketPair {
11267 start: "{".to_string(),
11268 end: "}".to_string(),
11269 close: true,
11270 surround: true,
11271 newline: true,
11272 },
11273 BracketPair {
11274 start: "(".to_string(),
11275 end: ")".to_string(),
11276 close: true,
11277 surround: true,
11278 newline: true,
11279 },
11280 BracketPair {
11281 start: "[".to_string(),
11282 end: "]".to_string(),
11283 close: false,
11284 surround: true,
11285 newline: true,
11286 },
11287 ],
11288 ..Default::default()
11289 },
11290 autoclose_before: "})]".to_string(),
11291 ..Default::default()
11292 },
11293 Some(tree_sitter_rust::LANGUAGE.into()),
11294 ));
11295
11296 cx.language_registry().add(language.clone());
11297 cx.update_buffer(|buffer, cx| {
11298 buffer.set_language(Some(language), cx);
11299 });
11300
11301 cx.set_state(
11302 &"
11303 {(ˇ)}
11304 [[ˇ]]
11305 {(ˇ)}
11306 "
11307 .unindent(),
11308 );
11309
11310 cx.update_editor(|editor, window, cx| {
11311 editor.backspace(&Default::default(), window, cx);
11312 editor.backspace(&Default::default(), window, cx);
11313 });
11314
11315 cx.assert_editor_state(
11316 &"
11317 ˇ
11318 ˇ]]
11319 ˇ
11320 "
11321 .unindent(),
11322 );
11323
11324 cx.update_editor(|editor, window, cx| {
11325 editor.handle_input("{", window, cx);
11326 editor.handle_input("{", window, cx);
11327 editor.move_right(&MoveRight, window, cx);
11328 editor.move_right(&MoveRight, window, cx);
11329 editor.move_left(&MoveLeft, window, cx);
11330 editor.move_left(&MoveLeft, window, cx);
11331 editor.backspace(&Default::default(), window, cx);
11332 });
11333
11334 cx.assert_editor_state(
11335 &"
11336 {ˇ}
11337 {ˇ}]]
11338 {ˇ}
11339 "
11340 .unindent(),
11341 );
11342
11343 cx.update_editor(|editor, window, cx| {
11344 editor.backspace(&Default::default(), window, cx);
11345 });
11346
11347 cx.assert_editor_state(
11348 &"
11349 ˇ
11350 ˇ]]
11351 ˇ
11352 "
11353 .unindent(),
11354 );
11355}
11356
11357#[gpui::test]
11358async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
11359 init_test(cx, |_| {});
11360
11361 let language = Arc::new(Language::new(
11362 LanguageConfig::default(),
11363 Some(tree_sitter_rust::LANGUAGE.into()),
11364 ));
11365
11366 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
11367 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11368 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11369 editor
11370 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11371 .await;
11372
11373 editor.update_in(cx, |editor, window, cx| {
11374 editor.set_auto_replace_emoji_shortcode(true);
11375
11376 editor.handle_input("Hello ", window, cx);
11377 editor.handle_input(":wave", window, cx);
11378 assert_eq!(editor.text(cx), "Hello :wave".unindent());
11379
11380 editor.handle_input(":", window, cx);
11381 assert_eq!(editor.text(cx), "Hello 👋".unindent());
11382
11383 editor.handle_input(" :smile", window, cx);
11384 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
11385
11386 editor.handle_input(":", window, cx);
11387 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
11388
11389 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
11390 editor.handle_input(":wave", window, cx);
11391 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
11392
11393 editor.handle_input(":", window, cx);
11394 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
11395
11396 editor.handle_input(":1", window, cx);
11397 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
11398
11399 editor.handle_input(":", window, cx);
11400 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11401
11402 // Ensure shortcode does not get replaced when it is part of a word
11403 editor.handle_input(" Test:wave", window, cx);
11404 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11405
11406 editor.handle_input(":", window, cx);
11407 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11408
11409 editor.set_auto_replace_emoji_shortcode(false);
11410
11411 // Ensure shortcode does not get replaced when auto replace is off
11412 editor.handle_input(" :wave", window, cx);
11413 assert_eq!(
11414 editor.text(cx),
11415 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11416 );
11417
11418 editor.handle_input(":", window, cx);
11419 assert_eq!(
11420 editor.text(cx),
11421 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11422 );
11423 });
11424}
11425
11426#[gpui::test]
11427async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11428 init_test(cx, |_| {});
11429
11430 let (text, insertion_ranges) = marked_text_ranges(
11431 indoc! {"
11432 ˇ
11433 "},
11434 false,
11435 );
11436
11437 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11438 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11439
11440 _ = editor.update_in(cx, |editor, window, cx| {
11441 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11442
11443 editor
11444 .insert_snippet(
11445 &insertion_ranges
11446 .iter()
11447 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11448 .collect::<Vec<_>>(),
11449 snippet,
11450 window,
11451 cx,
11452 )
11453 .unwrap();
11454
11455 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11456 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11457 assert_eq!(editor.text(cx), expected_text);
11458 assert_eq!(
11459 editor.selections.ranges(&editor.display_snapshot(cx)),
11460 selection_ranges
11461 .iter()
11462 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11463 .collect::<Vec<_>>()
11464 );
11465 }
11466
11467 assert(
11468 editor,
11469 cx,
11470 indoc! {"
11471 type «» =•
11472 "},
11473 );
11474
11475 assert!(editor.context_menu_visible(), "There should be a matches");
11476 });
11477}
11478
11479#[gpui::test]
11480async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
11481 init_test(cx, |_| {});
11482
11483 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11484 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11485 assert_eq!(editor.text(cx), expected_text);
11486 assert_eq!(
11487 editor.selections.ranges(&editor.display_snapshot(cx)),
11488 selection_ranges
11489 .iter()
11490 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11491 .collect::<Vec<_>>()
11492 );
11493 }
11494
11495 let (text, insertion_ranges) = marked_text_ranges(
11496 indoc! {"
11497 ˇ
11498 "},
11499 false,
11500 );
11501
11502 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11503 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11504
11505 _ = editor.update_in(cx, |editor, window, cx| {
11506 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
11507
11508 editor
11509 .insert_snippet(
11510 &insertion_ranges
11511 .iter()
11512 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11513 .collect::<Vec<_>>(),
11514 snippet,
11515 window,
11516 cx,
11517 )
11518 .unwrap();
11519
11520 assert_state(
11521 editor,
11522 cx,
11523 indoc! {"
11524 type «» = ;•
11525 "},
11526 );
11527
11528 assert!(
11529 editor.context_menu_visible(),
11530 "Context menu should be visible for placeholder choices"
11531 );
11532
11533 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11534
11535 assert_state(
11536 editor,
11537 cx,
11538 indoc! {"
11539 type = «»;•
11540 "},
11541 );
11542
11543 assert!(
11544 !editor.context_menu_visible(),
11545 "Context menu should be hidden after moving to next tabstop"
11546 );
11547
11548 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11549
11550 assert_state(
11551 editor,
11552 cx,
11553 indoc! {"
11554 type = ; ˇ
11555 "},
11556 );
11557
11558 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11559
11560 assert_state(
11561 editor,
11562 cx,
11563 indoc! {"
11564 type = ; ˇ
11565 "},
11566 );
11567 });
11568
11569 _ = editor.update_in(cx, |editor, window, cx| {
11570 editor.select_all(&SelectAll, window, cx);
11571 editor.backspace(&Backspace, window, cx);
11572
11573 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
11574 let insertion_ranges = editor
11575 .selections
11576 .all(&editor.display_snapshot(cx))
11577 .iter()
11578 .map(|s| s.range())
11579 .collect::<Vec<_>>();
11580
11581 editor
11582 .insert_snippet(&insertion_ranges, snippet, window, cx)
11583 .unwrap();
11584
11585 assert_state(editor, cx, "fn «» = value;•");
11586
11587 assert!(
11588 editor.context_menu_visible(),
11589 "Context menu should be visible for placeholder choices"
11590 );
11591
11592 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11593
11594 assert_state(editor, cx, "fn = «valueˇ»;•");
11595
11596 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11597
11598 assert_state(editor, cx, "fn «» = value;•");
11599
11600 assert!(
11601 editor.context_menu_visible(),
11602 "Context menu should be visible again after returning to first tabstop"
11603 );
11604
11605 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11606
11607 assert_state(editor, cx, "fn «» = value;•");
11608 });
11609}
11610
11611#[gpui::test]
11612async fn test_snippets(cx: &mut TestAppContext) {
11613 init_test(cx, |_| {});
11614
11615 let mut cx = EditorTestContext::new(cx).await;
11616
11617 cx.set_state(indoc! {"
11618 a.ˇ b
11619 a.ˇ b
11620 a.ˇ b
11621 "});
11622
11623 cx.update_editor(|editor, window, cx| {
11624 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11625 let insertion_ranges = editor
11626 .selections
11627 .all(&editor.display_snapshot(cx))
11628 .iter()
11629 .map(|s| s.range())
11630 .collect::<Vec<_>>();
11631 editor
11632 .insert_snippet(&insertion_ranges, snippet, window, cx)
11633 .unwrap();
11634 });
11635
11636 cx.assert_editor_state(indoc! {"
11637 a.f(«oneˇ», two, «threeˇ») b
11638 a.f(«oneˇ», two, «threeˇ») b
11639 a.f(«oneˇ», two, «threeˇ») b
11640 "});
11641
11642 // Can't move earlier than the first tab stop
11643 cx.update_editor(|editor, window, cx| {
11644 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11645 });
11646 cx.assert_editor_state(indoc! {"
11647 a.f(«oneˇ», two, «threeˇ») b
11648 a.f(«oneˇ», two, «threeˇ») b
11649 a.f(«oneˇ», two, «threeˇ») b
11650 "});
11651
11652 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11653 cx.assert_editor_state(indoc! {"
11654 a.f(one, «twoˇ», three) b
11655 a.f(one, «twoˇ», three) b
11656 a.f(one, «twoˇ», three) b
11657 "});
11658
11659 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11660 cx.assert_editor_state(indoc! {"
11661 a.f(«oneˇ», two, «threeˇ») b
11662 a.f(«oneˇ», two, «threeˇ») b
11663 a.f(«oneˇ», two, «threeˇ») b
11664 "});
11665
11666 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11667 cx.assert_editor_state(indoc! {"
11668 a.f(one, «twoˇ», three) b
11669 a.f(one, «twoˇ», three) b
11670 a.f(one, «twoˇ», three) b
11671 "});
11672 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11673 cx.assert_editor_state(indoc! {"
11674 a.f(one, two, three)ˇ b
11675 a.f(one, two, three)ˇ b
11676 a.f(one, two, three)ˇ b
11677 "});
11678
11679 // As soon as the last tab stop is reached, snippet state is gone
11680 cx.update_editor(|editor, window, cx| {
11681 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11682 });
11683 cx.assert_editor_state(indoc! {"
11684 a.f(one, two, three)ˇ b
11685 a.f(one, two, three)ˇ b
11686 a.f(one, two, three)ˇ b
11687 "});
11688}
11689
11690#[gpui::test]
11691async fn test_snippet_indentation(cx: &mut TestAppContext) {
11692 init_test(cx, |_| {});
11693
11694 let mut cx = EditorTestContext::new(cx).await;
11695
11696 cx.update_editor(|editor, window, cx| {
11697 let snippet = Snippet::parse(indoc! {"
11698 /*
11699 * Multiline comment with leading indentation
11700 *
11701 * $1
11702 */
11703 $0"})
11704 .unwrap();
11705 let insertion_ranges = editor
11706 .selections
11707 .all(&editor.display_snapshot(cx))
11708 .iter()
11709 .map(|s| s.range())
11710 .collect::<Vec<_>>();
11711 editor
11712 .insert_snippet(&insertion_ranges, snippet, window, cx)
11713 .unwrap();
11714 });
11715
11716 cx.assert_editor_state(indoc! {"
11717 /*
11718 * Multiline comment with leading indentation
11719 *
11720 * ˇ
11721 */
11722 "});
11723
11724 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11725 cx.assert_editor_state(indoc! {"
11726 /*
11727 * Multiline comment with leading indentation
11728 *
11729 *•
11730 */
11731 ˇ"});
11732}
11733
11734#[gpui::test]
11735async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
11736 init_test(cx, |_| {});
11737
11738 let mut cx = EditorTestContext::new(cx).await;
11739 cx.update_editor(|editor, _, cx| {
11740 editor.project().unwrap().update(cx, |project, cx| {
11741 project.snippets().update(cx, |snippets, _cx| {
11742 let snippet = project::snippet_provider::Snippet {
11743 prefix: vec!["multi word".to_string()],
11744 body: "this is many words".to_string(),
11745 description: Some("description".to_string()),
11746 name: "multi-word snippet test".to_string(),
11747 };
11748 snippets.add_snippet_for_test(
11749 None,
11750 PathBuf::from("test_snippets.json"),
11751 vec![Arc::new(snippet)],
11752 );
11753 });
11754 })
11755 });
11756
11757 for (input_to_simulate, should_match_snippet) in [
11758 ("m", true),
11759 ("m ", true),
11760 ("m w", true),
11761 ("aa m w", true),
11762 ("aa m g", false),
11763 ] {
11764 cx.set_state("ˇ");
11765 cx.simulate_input(input_to_simulate); // fails correctly
11766
11767 cx.update_editor(|editor, _, _| {
11768 let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
11769 else {
11770 assert!(!should_match_snippet); // no completions! don't even show the menu
11771 return;
11772 };
11773 assert!(context_menu.visible());
11774 let completions = context_menu.completions.borrow();
11775
11776 assert_eq!(!completions.is_empty(), should_match_snippet);
11777 });
11778 }
11779}
11780
11781#[gpui::test]
11782async fn test_document_format_during_save(cx: &mut TestAppContext) {
11783 init_test(cx, |_| {});
11784
11785 let fs = FakeFs::new(cx.executor());
11786 fs.insert_file(path!("/file.rs"), Default::default()).await;
11787
11788 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11789
11790 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11791 language_registry.add(rust_lang());
11792 let mut fake_servers = language_registry.register_fake_lsp(
11793 "Rust",
11794 FakeLspAdapter {
11795 capabilities: lsp::ServerCapabilities {
11796 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11797 ..Default::default()
11798 },
11799 ..Default::default()
11800 },
11801 );
11802
11803 let buffer = project
11804 .update(cx, |project, cx| {
11805 project.open_local_buffer(path!("/file.rs"), cx)
11806 })
11807 .await
11808 .unwrap();
11809
11810 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11811 let (editor, cx) = cx.add_window_view(|window, cx| {
11812 build_editor_with_project(project.clone(), buffer, window, cx)
11813 });
11814 editor.update_in(cx, |editor, window, cx| {
11815 editor.set_text("one\ntwo\nthree\n", window, cx)
11816 });
11817 assert!(cx.read(|cx| editor.is_dirty(cx)));
11818
11819 cx.executor().start_waiting();
11820 let fake_server = fake_servers.next().await.unwrap();
11821
11822 {
11823 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11824 move |params, _| async move {
11825 assert_eq!(
11826 params.text_document.uri,
11827 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11828 );
11829 assert_eq!(params.options.tab_size, 4);
11830 Ok(Some(vec![lsp::TextEdit::new(
11831 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11832 ", ".to_string(),
11833 )]))
11834 },
11835 );
11836 let save = editor
11837 .update_in(cx, |editor, window, cx| {
11838 editor.save(
11839 SaveOptions {
11840 format: true,
11841 autosave: false,
11842 },
11843 project.clone(),
11844 window,
11845 cx,
11846 )
11847 })
11848 .unwrap();
11849 cx.executor().start_waiting();
11850 save.await;
11851
11852 assert_eq!(
11853 editor.update(cx, |editor, cx| editor.text(cx)),
11854 "one, two\nthree\n"
11855 );
11856 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11857 }
11858
11859 {
11860 editor.update_in(cx, |editor, window, cx| {
11861 editor.set_text("one\ntwo\nthree\n", window, cx)
11862 });
11863 assert!(cx.read(|cx| editor.is_dirty(cx)));
11864
11865 // Ensure we can still save even if formatting hangs.
11866 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11867 move |params, _| async move {
11868 assert_eq!(
11869 params.text_document.uri,
11870 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11871 );
11872 futures::future::pending::<()>().await;
11873 unreachable!()
11874 },
11875 );
11876 let save = editor
11877 .update_in(cx, |editor, window, cx| {
11878 editor.save(
11879 SaveOptions {
11880 format: true,
11881 autosave: false,
11882 },
11883 project.clone(),
11884 window,
11885 cx,
11886 )
11887 })
11888 .unwrap();
11889 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11890 cx.executor().start_waiting();
11891 save.await;
11892 assert_eq!(
11893 editor.update(cx, |editor, cx| editor.text(cx)),
11894 "one\ntwo\nthree\n"
11895 );
11896 }
11897
11898 // Set rust language override and assert overridden tabsize is sent to language server
11899 update_test_language_settings(cx, |settings| {
11900 settings.languages.0.insert(
11901 "Rust".into(),
11902 LanguageSettingsContent {
11903 tab_size: NonZeroU32::new(8),
11904 ..Default::default()
11905 },
11906 );
11907 });
11908
11909 {
11910 editor.update_in(cx, |editor, window, cx| {
11911 editor.set_text("somehting_new\n", window, cx)
11912 });
11913 assert!(cx.read(|cx| editor.is_dirty(cx)));
11914 let _formatting_request_signal = fake_server
11915 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11916 assert_eq!(
11917 params.text_document.uri,
11918 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11919 );
11920 assert_eq!(params.options.tab_size, 8);
11921 Ok(Some(vec![]))
11922 });
11923 let save = editor
11924 .update_in(cx, |editor, window, cx| {
11925 editor.save(
11926 SaveOptions {
11927 format: true,
11928 autosave: false,
11929 },
11930 project.clone(),
11931 window,
11932 cx,
11933 )
11934 })
11935 .unwrap();
11936 cx.executor().start_waiting();
11937 save.await;
11938 }
11939}
11940
11941#[gpui::test]
11942async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11943 init_test(cx, |settings| {
11944 settings.defaults.ensure_final_newline_on_save = Some(false);
11945 });
11946
11947 let fs = FakeFs::new(cx.executor());
11948 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11949
11950 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11951
11952 let buffer = project
11953 .update(cx, |project, cx| {
11954 project.open_local_buffer(path!("/file.txt"), cx)
11955 })
11956 .await
11957 .unwrap();
11958
11959 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11960 let (editor, cx) = cx.add_window_view(|window, cx| {
11961 build_editor_with_project(project.clone(), buffer, window, cx)
11962 });
11963 editor.update_in(cx, |editor, window, cx| {
11964 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11965 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
11966 });
11967 });
11968 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11969
11970 editor.update_in(cx, |editor, window, cx| {
11971 editor.handle_input("\n", window, cx)
11972 });
11973 cx.run_until_parked();
11974 save(&editor, &project, cx).await;
11975 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11976
11977 editor.update_in(cx, |editor, window, cx| {
11978 editor.undo(&Default::default(), window, cx);
11979 });
11980 save(&editor, &project, cx).await;
11981 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11982
11983 editor.update_in(cx, |editor, window, cx| {
11984 editor.redo(&Default::default(), window, cx);
11985 });
11986 cx.run_until_parked();
11987 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11988
11989 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11990 let save = editor
11991 .update_in(cx, |editor, window, cx| {
11992 editor.save(
11993 SaveOptions {
11994 format: true,
11995 autosave: false,
11996 },
11997 project.clone(),
11998 window,
11999 cx,
12000 )
12001 })
12002 .unwrap();
12003 cx.executor().start_waiting();
12004 save.await;
12005 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12006 }
12007}
12008
12009#[gpui::test]
12010async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
12011 init_test(cx, |_| {});
12012
12013 let cols = 4;
12014 let rows = 10;
12015 let sample_text_1 = sample_text(rows, cols, 'a');
12016 assert_eq!(
12017 sample_text_1,
12018 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
12019 );
12020 let sample_text_2 = sample_text(rows, cols, 'l');
12021 assert_eq!(
12022 sample_text_2,
12023 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
12024 );
12025 let sample_text_3 = sample_text(rows, cols, 'v');
12026 assert_eq!(
12027 sample_text_3,
12028 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
12029 );
12030
12031 let fs = FakeFs::new(cx.executor());
12032 fs.insert_tree(
12033 path!("/a"),
12034 json!({
12035 "main.rs": sample_text_1,
12036 "other.rs": sample_text_2,
12037 "lib.rs": sample_text_3,
12038 }),
12039 )
12040 .await;
12041
12042 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12043 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12044 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12045
12046 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12047 language_registry.add(rust_lang());
12048 let mut fake_servers = language_registry.register_fake_lsp(
12049 "Rust",
12050 FakeLspAdapter {
12051 capabilities: lsp::ServerCapabilities {
12052 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12053 ..Default::default()
12054 },
12055 ..Default::default()
12056 },
12057 );
12058
12059 let worktree = project.update(cx, |project, cx| {
12060 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
12061 assert_eq!(worktrees.len(), 1);
12062 worktrees.pop().unwrap()
12063 });
12064 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12065
12066 let buffer_1 = project
12067 .update(cx, |project, cx| {
12068 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
12069 })
12070 .await
12071 .unwrap();
12072 let buffer_2 = project
12073 .update(cx, |project, cx| {
12074 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
12075 })
12076 .await
12077 .unwrap();
12078 let buffer_3 = project
12079 .update(cx, |project, cx| {
12080 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
12081 })
12082 .await
12083 .unwrap();
12084
12085 let multi_buffer = cx.new(|cx| {
12086 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12087 multi_buffer.push_excerpts(
12088 buffer_1.clone(),
12089 [
12090 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12091 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12092 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12093 ],
12094 cx,
12095 );
12096 multi_buffer.push_excerpts(
12097 buffer_2.clone(),
12098 [
12099 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12100 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12101 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12102 ],
12103 cx,
12104 );
12105 multi_buffer.push_excerpts(
12106 buffer_3.clone(),
12107 [
12108 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12109 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12110 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12111 ],
12112 cx,
12113 );
12114 multi_buffer
12115 });
12116 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
12117 Editor::new(
12118 EditorMode::full(),
12119 multi_buffer,
12120 Some(project.clone()),
12121 window,
12122 cx,
12123 )
12124 });
12125
12126 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12127 editor.change_selections(
12128 SelectionEffects::scroll(Autoscroll::Next),
12129 window,
12130 cx,
12131 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
12132 );
12133 editor.insert("|one|two|three|", window, cx);
12134 });
12135 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
12136 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12137 editor.change_selections(
12138 SelectionEffects::scroll(Autoscroll::Next),
12139 window,
12140 cx,
12141 |s| s.select_ranges(Some(MultiBufferOffset(60)..MultiBufferOffset(70))),
12142 );
12143 editor.insert("|four|five|six|", window, cx);
12144 });
12145 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
12146
12147 // First two buffers should be edited, but not the third one.
12148 assert_eq!(
12149 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
12150 "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}",
12151 );
12152 buffer_1.update(cx, |buffer, _| {
12153 assert!(buffer.is_dirty());
12154 assert_eq!(
12155 buffer.text(),
12156 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
12157 )
12158 });
12159 buffer_2.update(cx, |buffer, _| {
12160 assert!(buffer.is_dirty());
12161 assert_eq!(
12162 buffer.text(),
12163 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
12164 )
12165 });
12166 buffer_3.update(cx, |buffer, _| {
12167 assert!(!buffer.is_dirty());
12168 assert_eq!(buffer.text(), sample_text_3,)
12169 });
12170 cx.executor().run_until_parked();
12171
12172 cx.executor().start_waiting();
12173 let save = multi_buffer_editor
12174 .update_in(cx, |editor, window, cx| {
12175 editor.save(
12176 SaveOptions {
12177 format: true,
12178 autosave: false,
12179 },
12180 project.clone(),
12181 window,
12182 cx,
12183 )
12184 })
12185 .unwrap();
12186
12187 let fake_server = fake_servers.next().await.unwrap();
12188 fake_server
12189 .server
12190 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
12191 Ok(Some(vec![lsp::TextEdit::new(
12192 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12193 format!("[{} formatted]", params.text_document.uri),
12194 )]))
12195 })
12196 .detach();
12197 save.await;
12198
12199 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
12200 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
12201 assert_eq!(
12202 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
12203 uri!(
12204 "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}"
12205 ),
12206 );
12207 buffer_1.update(cx, |buffer, _| {
12208 assert!(!buffer.is_dirty());
12209 assert_eq!(
12210 buffer.text(),
12211 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
12212 )
12213 });
12214 buffer_2.update(cx, |buffer, _| {
12215 assert!(!buffer.is_dirty());
12216 assert_eq!(
12217 buffer.text(),
12218 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
12219 )
12220 });
12221 buffer_3.update(cx, |buffer, _| {
12222 assert!(!buffer.is_dirty());
12223 assert_eq!(buffer.text(), sample_text_3,)
12224 });
12225}
12226
12227#[gpui::test]
12228async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
12229 init_test(cx, |_| {});
12230
12231 let fs = FakeFs::new(cx.executor());
12232 fs.insert_tree(
12233 path!("/dir"),
12234 json!({
12235 "file1.rs": "fn main() { println!(\"hello\"); }",
12236 "file2.rs": "fn test() { println!(\"test\"); }",
12237 "file3.rs": "fn other() { println!(\"other\"); }\n",
12238 }),
12239 )
12240 .await;
12241
12242 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
12243 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12244 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12245
12246 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12247 language_registry.add(rust_lang());
12248
12249 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
12250 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12251
12252 // Open three buffers
12253 let buffer_1 = project
12254 .update(cx, |project, cx| {
12255 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
12256 })
12257 .await
12258 .unwrap();
12259 let buffer_2 = project
12260 .update(cx, |project, cx| {
12261 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
12262 })
12263 .await
12264 .unwrap();
12265 let buffer_3 = project
12266 .update(cx, |project, cx| {
12267 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
12268 })
12269 .await
12270 .unwrap();
12271
12272 // Create a multi-buffer with all three buffers
12273 let multi_buffer = cx.new(|cx| {
12274 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12275 multi_buffer.push_excerpts(
12276 buffer_1.clone(),
12277 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12278 cx,
12279 );
12280 multi_buffer.push_excerpts(
12281 buffer_2.clone(),
12282 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12283 cx,
12284 );
12285 multi_buffer.push_excerpts(
12286 buffer_3.clone(),
12287 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12288 cx,
12289 );
12290 multi_buffer
12291 });
12292
12293 let editor = cx.new_window_entity(|window, cx| {
12294 Editor::new(
12295 EditorMode::full(),
12296 multi_buffer,
12297 Some(project.clone()),
12298 window,
12299 cx,
12300 )
12301 });
12302
12303 // Edit only the first buffer
12304 editor.update_in(cx, |editor, window, cx| {
12305 editor.change_selections(
12306 SelectionEffects::scroll(Autoscroll::Next),
12307 window,
12308 cx,
12309 |s| s.select_ranges(Some(MultiBufferOffset(10)..MultiBufferOffset(10))),
12310 );
12311 editor.insert("// edited", window, cx);
12312 });
12313
12314 // Verify that only buffer 1 is dirty
12315 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
12316 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12317 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12318
12319 // Get write counts after file creation (files were created with initial content)
12320 // We expect each file to have been written once during creation
12321 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
12322 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
12323 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
12324
12325 // Perform autosave
12326 let save_task = editor.update_in(cx, |editor, window, cx| {
12327 editor.save(
12328 SaveOptions {
12329 format: true,
12330 autosave: true,
12331 },
12332 project.clone(),
12333 window,
12334 cx,
12335 )
12336 });
12337 save_task.await.unwrap();
12338
12339 // Only the dirty buffer should have been saved
12340 assert_eq!(
12341 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12342 1,
12343 "Buffer 1 was dirty, so it should have been written once during autosave"
12344 );
12345 assert_eq!(
12346 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12347 0,
12348 "Buffer 2 was clean, so it should not have been written during autosave"
12349 );
12350 assert_eq!(
12351 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12352 0,
12353 "Buffer 3 was clean, so it should not have been written during autosave"
12354 );
12355
12356 // Verify buffer states after autosave
12357 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12358 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12359 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12360
12361 // Now perform a manual save (format = true)
12362 let save_task = editor.update_in(cx, |editor, window, cx| {
12363 editor.save(
12364 SaveOptions {
12365 format: true,
12366 autosave: false,
12367 },
12368 project.clone(),
12369 window,
12370 cx,
12371 )
12372 });
12373 save_task.await.unwrap();
12374
12375 // During manual save, clean buffers don't get written to disk
12376 // They just get did_save called for language server notifications
12377 assert_eq!(
12378 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12379 1,
12380 "Buffer 1 should only have been written once total (during autosave, not manual save)"
12381 );
12382 assert_eq!(
12383 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12384 0,
12385 "Buffer 2 should not have been written at all"
12386 );
12387 assert_eq!(
12388 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12389 0,
12390 "Buffer 3 should not have been written at all"
12391 );
12392}
12393
12394async fn setup_range_format_test(
12395 cx: &mut TestAppContext,
12396) -> (
12397 Entity<Project>,
12398 Entity<Editor>,
12399 &mut gpui::VisualTestContext,
12400 lsp::FakeLanguageServer,
12401) {
12402 init_test(cx, |_| {});
12403
12404 let fs = FakeFs::new(cx.executor());
12405 fs.insert_file(path!("/file.rs"), Default::default()).await;
12406
12407 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12408
12409 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12410 language_registry.add(rust_lang());
12411 let mut fake_servers = language_registry.register_fake_lsp(
12412 "Rust",
12413 FakeLspAdapter {
12414 capabilities: lsp::ServerCapabilities {
12415 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
12416 ..lsp::ServerCapabilities::default()
12417 },
12418 ..FakeLspAdapter::default()
12419 },
12420 );
12421
12422 let buffer = project
12423 .update(cx, |project, cx| {
12424 project.open_local_buffer(path!("/file.rs"), cx)
12425 })
12426 .await
12427 .unwrap();
12428
12429 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12430 let (editor, cx) = cx.add_window_view(|window, cx| {
12431 build_editor_with_project(project.clone(), buffer, window, cx)
12432 });
12433
12434 cx.executor().start_waiting();
12435 let fake_server = fake_servers.next().await.unwrap();
12436
12437 (project, editor, cx, fake_server)
12438}
12439
12440#[gpui::test]
12441async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
12442 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12443
12444 editor.update_in(cx, |editor, window, cx| {
12445 editor.set_text("one\ntwo\nthree\n", window, cx)
12446 });
12447 assert!(cx.read(|cx| editor.is_dirty(cx)));
12448
12449 let save = editor
12450 .update_in(cx, |editor, window, cx| {
12451 editor.save(
12452 SaveOptions {
12453 format: true,
12454 autosave: false,
12455 },
12456 project.clone(),
12457 window,
12458 cx,
12459 )
12460 })
12461 .unwrap();
12462 fake_server
12463 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12464 assert_eq!(
12465 params.text_document.uri,
12466 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12467 );
12468 assert_eq!(params.options.tab_size, 4);
12469 Ok(Some(vec![lsp::TextEdit::new(
12470 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12471 ", ".to_string(),
12472 )]))
12473 })
12474 .next()
12475 .await;
12476 cx.executor().start_waiting();
12477 save.await;
12478 assert_eq!(
12479 editor.update(cx, |editor, cx| editor.text(cx)),
12480 "one, two\nthree\n"
12481 );
12482 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12483}
12484
12485#[gpui::test]
12486async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
12487 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12488
12489 editor.update_in(cx, |editor, window, cx| {
12490 editor.set_text("one\ntwo\nthree\n", window, cx)
12491 });
12492 assert!(cx.read(|cx| editor.is_dirty(cx)));
12493
12494 // Test that save still works when formatting hangs
12495 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
12496 move |params, _| async move {
12497 assert_eq!(
12498 params.text_document.uri,
12499 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12500 );
12501 futures::future::pending::<()>().await;
12502 unreachable!()
12503 },
12504 );
12505 let save = editor
12506 .update_in(cx, |editor, window, cx| {
12507 editor.save(
12508 SaveOptions {
12509 format: true,
12510 autosave: false,
12511 },
12512 project.clone(),
12513 window,
12514 cx,
12515 )
12516 })
12517 .unwrap();
12518 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12519 cx.executor().start_waiting();
12520 save.await;
12521 assert_eq!(
12522 editor.update(cx, |editor, cx| editor.text(cx)),
12523 "one\ntwo\nthree\n"
12524 );
12525 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12526}
12527
12528#[gpui::test]
12529async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
12530 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12531
12532 // Buffer starts clean, no formatting should be requested
12533 let save = editor
12534 .update_in(cx, |editor, window, cx| {
12535 editor.save(
12536 SaveOptions {
12537 format: false,
12538 autosave: false,
12539 },
12540 project.clone(),
12541 window,
12542 cx,
12543 )
12544 })
12545 .unwrap();
12546 let _pending_format_request = fake_server
12547 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12548 panic!("Should not be invoked");
12549 })
12550 .next();
12551 cx.executor().start_waiting();
12552 save.await;
12553 cx.run_until_parked();
12554}
12555
12556#[gpui::test]
12557async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12558 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12559
12560 // Set Rust language override and assert overridden tabsize is sent to language server
12561 update_test_language_settings(cx, |settings| {
12562 settings.languages.0.insert(
12563 "Rust".into(),
12564 LanguageSettingsContent {
12565 tab_size: NonZeroU32::new(8),
12566 ..Default::default()
12567 },
12568 );
12569 });
12570
12571 editor.update_in(cx, |editor, window, cx| {
12572 editor.set_text("something_new\n", window, cx)
12573 });
12574 assert!(cx.read(|cx| editor.is_dirty(cx)));
12575 let save = editor
12576 .update_in(cx, |editor, window, cx| {
12577 editor.save(
12578 SaveOptions {
12579 format: true,
12580 autosave: false,
12581 },
12582 project.clone(),
12583 window,
12584 cx,
12585 )
12586 })
12587 .unwrap();
12588 fake_server
12589 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12590 assert_eq!(
12591 params.text_document.uri,
12592 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12593 );
12594 assert_eq!(params.options.tab_size, 8);
12595 Ok(Some(Vec::new()))
12596 })
12597 .next()
12598 .await;
12599 save.await;
12600}
12601
12602#[gpui::test]
12603async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12604 init_test(cx, |settings| {
12605 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12606 settings::LanguageServerFormatterSpecifier::Current,
12607 )))
12608 });
12609
12610 let fs = FakeFs::new(cx.executor());
12611 fs.insert_file(path!("/file.rs"), Default::default()).await;
12612
12613 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12614
12615 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12616 language_registry.add(Arc::new(Language::new(
12617 LanguageConfig {
12618 name: "Rust".into(),
12619 matcher: LanguageMatcher {
12620 path_suffixes: vec!["rs".to_string()],
12621 ..Default::default()
12622 },
12623 ..LanguageConfig::default()
12624 },
12625 Some(tree_sitter_rust::LANGUAGE.into()),
12626 )));
12627 update_test_language_settings(cx, |settings| {
12628 // Enable Prettier formatting for the same buffer, and ensure
12629 // LSP is called instead of Prettier.
12630 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12631 });
12632 let mut fake_servers = language_registry.register_fake_lsp(
12633 "Rust",
12634 FakeLspAdapter {
12635 capabilities: lsp::ServerCapabilities {
12636 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12637 ..Default::default()
12638 },
12639 ..Default::default()
12640 },
12641 );
12642
12643 let buffer = project
12644 .update(cx, |project, cx| {
12645 project.open_local_buffer(path!("/file.rs"), cx)
12646 })
12647 .await
12648 .unwrap();
12649
12650 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12651 let (editor, cx) = cx.add_window_view(|window, cx| {
12652 build_editor_with_project(project.clone(), buffer, window, cx)
12653 });
12654 editor.update_in(cx, |editor, window, cx| {
12655 editor.set_text("one\ntwo\nthree\n", window, cx)
12656 });
12657
12658 cx.executor().start_waiting();
12659 let fake_server = fake_servers.next().await.unwrap();
12660
12661 let format = editor
12662 .update_in(cx, |editor, window, cx| {
12663 editor.perform_format(
12664 project.clone(),
12665 FormatTrigger::Manual,
12666 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12667 window,
12668 cx,
12669 )
12670 })
12671 .unwrap();
12672 fake_server
12673 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12674 assert_eq!(
12675 params.text_document.uri,
12676 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12677 );
12678 assert_eq!(params.options.tab_size, 4);
12679 Ok(Some(vec![lsp::TextEdit::new(
12680 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12681 ", ".to_string(),
12682 )]))
12683 })
12684 .next()
12685 .await;
12686 cx.executor().start_waiting();
12687 format.await;
12688 assert_eq!(
12689 editor.update(cx, |editor, cx| editor.text(cx)),
12690 "one, two\nthree\n"
12691 );
12692
12693 editor.update_in(cx, |editor, window, cx| {
12694 editor.set_text("one\ntwo\nthree\n", window, cx)
12695 });
12696 // Ensure we don't lock if formatting hangs.
12697 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12698 move |params, _| async move {
12699 assert_eq!(
12700 params.text_document.uri,
12701 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12702 );
12703 futures::future::pending::<()>().await;
12704 unreachable!()
12705 },
12706 );
12707 let format = editor
12708 .update_in(cx, |editor, window, cx| {
12709 editor.perform_format(
12710 project,
12711 FormatTrigger::Manual,
12712 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12713 window,
12714 cx,
12715 )
12716 })
12717 .unwrap();
12718 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12719 cx.executor().start_waiting();
12720 format.await;
12721 assert_eq!(
12722 editor.update(cx, |editor, cx| editor.text(cx)),
12723 "one\ntwo\nthree\n"
12724 );
12725}
12726
12727#[gpui::test]
12728async fn test_multiple_formatters(cx: &mut TestAppContext) {
12729 init_test(cx, |settings| {
12730 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12731 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12732 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12733 Formatter::CodeAction("code-action-1".into()),
12734 Formatter::CodeAction("code-action-2".into()),
12735 ]))
12736 });
12737
12738 let fs = FakeFs::new(cx.executor());
12739 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12740 .await;
12741
12742 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12743 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12744 language_registry.add(rust_lang());
12745
12746 let mut fake_servers = language_registry.register_fake_lsp(
12747 "Rust",
12748 FakeLspAdapter {
12749 capabilities: lsp::ServerCapabilities {
12750 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12751 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12752 commands: vec!["the-command-for-code-action-1".into()],
12753 ..Default::default()
12754 }),
12755 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12756 ..Default::default()
12757 },
12758 ..Default::default()
12759 },
12760 );
12761
12762 let buffer = project
12763 .update(cx, |project, cx| {
12764 project.open_local_buffer(path!("/file.rs"), cx)
12765 })
12766 .await
12767 .unwrap();
12768
12769 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12770 let (editor, cx) = cx.add_window_view(|window, cx| {
12771 build_editor_with_project(project.clone(), buffer, window, cx)
12772 });
12773
12774 cx.executor().start_waiting();
12775
12776 let fake_server = fake_servers.next().await.unwrap();
12777 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12778 move |_params, _| async move {
12779 Ok(Some(vec![lsp::TextEdit::new(
12780 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12781 "applied-formatting\n".to_string(),
12782 )]))
12783 },
12784 );
12785 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12786 move |params, _| async move {
12787 let requested_code_actions = params.context.only.expect("Expected code action request");
12788 assert_eq!(requested_code_actions.len(), 1);
12789
12790 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12791 let code_action = match requested_code_actions[0].as_str() {
12792 "code-action-1" => lsp::CodeAction {
12793 kind: Some("code-action-1".into()),
12794 edit: Some(lsp::WorkspaceEdit::new(
12795 [(
12796 uri,
12797 vec![lsp::TextEdit::new(
12798 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12799 "applied-code-action-1-edit\n".to_string(),
12800 )],
12801 )]
12802 .into_iter()
12803 .collect(),
12804 )),
12805 command: Some(lsp::Command {
12806 command: "the-command-for-code-action-1".into(),
12807 ..Default::default()
12808 }),
12809 ..Default::default()
12810 },
12811 "code-action-2" => lsp::CodeAction {
12812 kind: Some("code-action-2".into()),
12813 edit: Some(lsp::WorkspaceEdit::new(
12814 [(
12815 uri,
12816 vec![lsp::TextEdit::new(
12817 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12818 "applied-code-action-2-edit\n".to_string(),
12819 )],
12820 )]
12821 .into_iter()
12822 .collect(),
12823 )),
12824 ..Default::default()
12825 },
12826 req => panic!("Unexpected code action request: {:?}", req),
12827 };
12828 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12829 code_action,
12830 )]))
12831 },
12832 );
12833
12834 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12835 move |params, _| async move { Ok(params) }
12836 });
12837
12838 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12839 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12840 let fake = fake_server.clone();
12841 let lock = command_lock.clone();
12842 move |params, _| {
12843 assert_eq!(params.command, "the-command-for-code-action-1");
12844 let fake = fake.clone();
12845 let lock = lock.clone();
12846 async move {
12847 lock.lock().await;
12848 fake.server
12849 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12850 label: None,
12851 edit: lsp::WorkspaceEdit {
12852 changes: Some(
12853 [(
12854 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12855 vec![lsp::TextEdit {
12856 range: lsp::Range::new(
12857 lsp::Position::new(0, 0),
12858 lsp::Position::new(0, 0),
12859 ),
12860 new_text: "applied-code-action-1-command\n".into(),
12861 }],
12862 )]
12863 .into_iter()
12864 .collect(),
12865 ),
12866 ..Default::default()
12867 },
12868 })
12869 .await
12870 .into_response()
12871 .unwrap();
12872 Ok(Some(json!(null)))
12873 }
12874 }
12875 });
12876
12877 cx.executor().start_waiting();
12878 editor
12879 .update_in(cx, |editor, window, cx| {
12880 editor.perform_format(
12881 project.clone(),
12882 FormatTrigger::Manual,
12883 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12884 window,
12885 cx,
12886 )
12887 })
12888 .unwrap()
12889 .await;
12890 editor.update(cx, |editor, cx| {
12891 assert_eq!(
12892 editor.text(cx),
12893 r#"
12894 applied-code-action-2-edit
12895 applied-code-action-1-command
12896 applied-code-action-1-edit
12897 applied-formatting
12898 one
12899 two
12900 three
12901 "#
12902 .unindent()
12903 );
12904 });
12905
12906 editor.update_in(cx, |editor, window, cx| {
12907 editor.undo(&Default::default(), window, cx);
12908 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12909 });
12910
12911 // Perform a manual edit while waiting for an LSP command
12912 // that's being run as part of a formatting code action.
12913 let lock_guard = command_lock.lock().await;
12914 let format = editor
12915 .update_in(cx, |editor, window, cx| {
12916 editor.perform_format(
12917 project.clone(),
12918 FormatTrigger::Manual,
12919 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12920 window,
12921 cx,
12922 )
12923 })
12924 .unwrap();
12925 cx.run_until_parked();
12926 editor.update(cx, |editor, cx| {
12927 assert_eq!(
12928 editor.text(cx),
12929 r#"
12930 applied-code-action-1-edit
12931 applied-formatting
12932 one
12933 two
12934 three
12935 "#
12936 .unindent()
12937 );
12938
12939 editor.buffer.update(cx, |buffer, cx| {
12940 let ix = buffer.len(cx);
12941 buffer.edit([(ix..ix, "edited\n")], None, cx);
12942 });
12943 });
12944
12945 // Allow the LSP command to proceed. Because the buffer was edited,
12946 // the second code action will not be run.
12947 drop(lock_guard);
12948 format.await;
12949 editor.update_in(cx, |editor, window, cx| {
12950 assert_eq!(
12951 editor.text(cx),
12952 r#"
12953 applied-code-action-1-command
12954 applied-code-action-1-edit
12955 applied-formatting
12956 one
12957 two
12958 three
12959 edited
12960 "#
12961 .unindent()
12962 );
12963
12964 // The manual edit is undone first, because it is the last thing the user did
12965 // (even though the command completed afterwards).
12966 editor.undo(&Default::default(), window, cx);
12967 assert_eq!(
12968 editor.text(cx),
12969 r#"
12970 applied-code-action-1-command
12971 applied-code-action-1-edit
12972 applied-formatting
12973 one
12974 two
12975 three
12976 "#
12977 .unindent()
12978 );
12979
12980 // All the formatting (including the command, which completed after the manual edit)
12981 // is undone together.
12982 editor.undo(&Default::default(), window, cx);
12983 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12984 });
12985}
12986
12987#[gpui::test]
12988async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12989 init_test(cx, |settings| {
12990 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12991 settings::LanguageServerFormatterSpecifier::Current,
12992 )]))
12993 });
12994
12995 let fs = FakeFs::new(cx.executor());
12996 fs.insert_file(path!("/file.ts"), Default::default()).await;
12997
12998 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12999
13000 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13001 language_registry.add(Arc::new(Language::new(
13002 LanguageConfig {
13003 name: "TypeScript".into(),
13004 matcher: LanguageMatcher {
13005 path_suffixes: vec!["ts".to_string()],
13006 ..Default::default()
13007 },
13008 ..LanguageConfig::default()
13009 },
13010 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13011 )));
13012 update_test_language_settings(cx, |settings| {
13013 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
13014 });
13015 let mut fake_servers = language_registry.register_fake_lsp(
13016 "TypeScript",
13017 FakeLspAdapter {
13018 capabilities: lsp::ServerCapabilities {
13019 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
13020 ..Default::default()
13021 },
13022 ..Default::default()
13023 },
13024 );
13025
13026 let buffer = project
13027 .update(cx, |project, cx| {
13028 project.open_local_buffer(path!("/file.ts"), cx)
13029 })
13030 .await
13031 .unwrap();
13032
13033 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13034 let (editor, cx) = cx.add_window_view(|window, cx| {
13035 build_editor_with_project(project.clone(), buffer, window, cx)
13036 });
13037 editor.update_in(cx, |editor, window, cx| {
13038 editor.set_text(
13039 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
13040 window,
13041 cx,
13042 )
13043 });
13044
13045 cx.executor().start_waiting();
13046 let fake_server = fake_servers.next().await.unwrap();
13047
13048 let format = editor
13049 .update_in(cx, |editor, window, cx| {
13050 editor.perform_code_action_kind(
13051 project.clone(),
13052 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
13053 window,
13054 cx,
13055 )
13056 })
13057 .unwrap();
13058 fake_server
13059 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
13060 assert_eq!(
13061 params.text_document.uri,
13062 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
13063 );
13064 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
13065 lsp::CodeAction {
13066 title: "Organize Imports".to_string(),
13067 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
13068 edit: Some(lsp::WorkspaceEdit {
13069 changes: Some(
13070 [(
13071 params.text_document.uri.clone(),
13072 vec![lsp::TextEdit::new(
13073 lsp::Range::new(
13074 lsp::Position::new(1, 0),
13075 lsp::Position::new(2, 0),
13076 ),
13077 "".to_string(),
13078 )],
13079 )]
13080 .into_iter()
13081 .collect(),
13082 ),
13083 ..Default::default()
13084 }),
13085 ..Default::default()
13086 },
13087 )]))
13088 })
13089 .next()
13090 .await;
13091 cx.executor().start_waiting();
13092 format.await;
13093 assert_eq!(
13094 editor.update(cx, |editor, cx| editor.text(cx)),
13095 "import { a } from 'module';\n\nconst x = a;\n"
13096 );
13097
13098 editor.update_in(cx, |editor, window, cx| {
13099 editor.set_text(
13100 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
13101 window,
13102 cx,
13103 )
13104 });
13105 // Ensure we don't lock if code action hangs.
13106 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
13107 move |params, _| async move {
13108 assert_eq!(
13109 params.text_document.uri,
13110 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
13111 );
13112 futures::future::pending::<()>().await;
13113 unreachable!()
13114 },
13115 );
13116 let format = editor
13117 .update_in(cx, |editor, window, cx| {
13118 editor.perform_code_action_kind(
13119 project,
13120 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
13121 window,
13122 cx,
13123 )
13124 })
13125 .unwrap();
13126 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
13127 cx.executor().start_waiting();
13128 format.await;
13129 assert_eq!(
13130 editor.update(cx, |editor, cx| editor.text(cx)),
13131 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
13132 );
13133}
13134
13135#[gpui::test]
13136async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
13137 init_test(cx, |_| {});
13138
13139 let mut cx = EditorLspTestContext::new_rust(
13140 lsp::ServerCapabilities {
13141 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13142 ..Default::default()
13143 },
13144 cx,
13145 )
13146 .await;
13147
13148 cx.set_state(indoc! {"
13149 one.twoˇ
13150 "});
13151
13152 // The format request takes a long time. When it completes, it inserts
13153 // a newline and an indent before the `.`
13154 cx.lsp
13155 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
13156 let executor = cx.background_executor().clone();
13157 async move {
13158 executor.timer(Duration::from_millis(100)).await;
13159 Ok(Some(vec![lsp::TextEdit {
13160 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
13161 new_text: "\n ".into(),
13162 }]))
13163 }
13164 });
13165
13166 // Submit a format request.
13167 let format_1 = cx
13168 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13169 .unwrap();
13170 cx.executor().run_until_parked();
13171
13172 // Submit a second format request.
13173 let format_2 = cx
13174 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13175 .unwrap();
13176 cx.executor().run_until_parked();
13177
13178 // Wait for both format requests to complete
13179 cx.executor().advance_clock(Duration::from_millis(200));
13180 cx.executor().start_waiting();
13181 format_1.await.unwrap();
13182 cx.executor().start_waiting();
13183 format_2.await.unwrap();
13184
13185 // The formatting edits only happens once.
13186 cx.assert_editor_state(indoc! {"
13187 one
13188 .twoˇ
13189 "});
13190}
13191
13192#[gpui::test]
13193async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
13194 init_test(cx, |settings| {
13195 settings.defaults.formatter = Some(FormatterList::default())
13196 });
13197
13198 let mut cx = EditorLspTestContext::new_rust(
13199 lsp::ServerCapabilities {
13200 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13201 ..Default::default()
13202 },
13203 cx,
13204 )
13205 .await;
13206
13207 // Record which buffer changes have been sent to the language server
13208 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
13209 cx.lsp
13210 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
13211 let buffer_changes = buffer_changes.clone();
13212 move |params, _| {
13213 buffer_changes.lock().extend(
13214 params
13215 .content_changes
13216 .into_iter()
13217 .map(|e| (e.range.unwrap(), e.text)),
13218 );
13219 }
13220 });
13221 // Handle formatting requests to the language server.
13222 cx.lsp
13223 .set_request_handler::<lsp::request::Formatting, _, _>({
13224 let buffer_changes = buffer_changes.clone();
13225 move |_, _| {
13226 let buffer_changes = buffer_changes.clone();
13227 // Insert blank lines between each line of the buffer.
13228 async move {
13229 // When formatting is requested, trailing whitespace has already been stripped,
13230 // and the trailing newline has already been added.
13231 assert_eq!(
13232 &buffer_changes.lock()[1..],
13233 &[
13234 (
13235 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
13236 "".into()
13237 ),
13238 (
13239 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
13240 "".into()
13241 ),
13242 (
13243 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
13244 "\n".into()
13245 ),
13246 ]
13247 );
13248
13249 Ok(Some(vec![
13250 lsp::TextEdit {
13251 range: lsp::Range::new(
13252 lsp::Position::new(1, 0),
13253 lsp::Position::new(1, 0),
13254 ),
13255 new_text: "\n".into(),
13256 },
13257 lsp::TextEdit {
13258 range: lsp::Range::new(
13259 lsp::Position::new(2, 0),
13260 lsp::Position::new(2, 0),
13261 ),
13262 new_text: "\n".into(),
13263 },
13264 ]))
13265 }
13266 }
13267 });
13268
13269 // Set up a buffer white some trailing whitespace and no trailing newline.
13270 cx.set_state(
13271 &[
13272 "one ", //
13273 "twoˇ", //
13274 "three ", //
13275 "four", //
13276 ]
13277 .join("\n"),
13278 );
13279 cx.run_until_parked();
13280
13281 // Submit a format request.
13282 let format = cx
13283 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13284 .unwrap();
13285
13286 cx.run_until_parked();
13287 // After formatting the buffer, the trailing whitespace is stripped,
13288 // a newline is appended, and the edits provided by the language server
13289 // have been applied.
13290 format.await.unwrap();
13291
13292 cx.assert_editor_state(
13293 &[
13294 "one", //
13295 "", //
13296 "twoˇ", //
13297 "", //
13298 "three", //
13299 "four", //
13300 "", //
13301 ]
13302 .join("\n"),
13303 );
13304
13305 // Undoing the formatting undoes the trailing whitespace removal, the
13306 // trailing newline, and the LSP edits.
13307 cx.update_buffer(|buffer, cx| buffer.undo(cx));
13308 cx.assert_editor_state(
13309 &[
13310 "one ", //
13311 "twoˇ", //
13312 "three ", //
13313 "four", //
13314 ]
13315 .join("\n"),
13316 );
13317}
13318
13319#[gpui::test]
13320async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
13321 cx: &mut TestAppContext,
13322) {
13323 init_test(cx, |_| {});
13324
13325 cx.update(|cx| {
13326 cx.update_global::<SettingsStore, _>(|settings, cx| {
13327 settings.update_user_settings(cx, |settings| {
13328 settings.editor.auto_signature_help = Some(true);
13329 });
13330 });
13331 });
13332
13333 let mut cx = EditorLspTestContext::new_rust(
13334 lsp::ServerCapabilities {
13335 signature_help_provider: Some(lsp::SignatureHelpOptions {
13336 ..Default::default()
13337 }),
13338 ..Default::default()
13339 },
13340 cx,
13341 )
13342 .await;
13343
13344 let language = Language::new(
13345 LanguageConfig {
13346 name: "Rust".into(),
13347 brackets: BracketPairConfig {
13348 pairs: vec![
13349 BracketPair {
13350 start: "{".to_string(),
13351 end: "}".to_string(),
13352 close: true,
13353 surround: true,
13354 newline: true,
13355 },
13356 BracketPair {
13357 start: "(".to_string(),
13358 end: ")".to_string(),
13359 close: true,
13360 surround: true,
13361 newline: true,
13362 },
13363 BracketPair {
13364 start: "/*".to_string(),
13365 end: " */".to_string(),
13366 close: true,
13367 surround: true,
13368 newline: true,
13369 },
13370 BracketPair {
13371 start: "[".to_string(),
13372 end: "]".to_string(),
13373 close: false,
13374 surround: false,
13375 newline: true,
13376 },
13377 BracketPair {
13378 start: "\"".to_string(),
13379 end: "\"".to_string(),
13380 close: true,
13381 surround: true,
13382 newline: false,
13383 },
13384 BracketPair {
13385 start: "<".to_string(),
13386 end: ">".to_string(),
13387 close: false,
13388 surround: true,
13389 newline: true,
13390 },
13391 ],
13392 ..Default::default()
13393 },
13394 autoclose_before: "})]".to_string(),
13395 ..Default::default()
13396 },
13397 Some(tree_sitter_rust::LANGUAGE.into()),
13398 );
13399 let language = Arc::new(language);
13400
13401 cx.language_registry().add(language.clone());
13402 cx.update_buffer(|buffer, cx| {
13403 buffer.set_language(Some(language), cx);
13404 });
13405
13406 cx.set_state(
13407 &r#"
13408 fn main() {
13409 sampleˇ
13410 }
13411 "#
13412 .unindent(),
13413 );
13414
13415 cx.update_editor(|editor, window, cx| {
13416 editor.handle_input("(", window, cx);
13417 });
13418 cx.assert_editor_state(
13419 &"
13420 fn main() {
13421 sample(ˇ)
13422 }
13423 "
13424 .unindent(),
13425 );
13426
13427 let mocked_response = lsp::SignatureHelp {
13428 signatures: vec![lsp::SignatureInformation {
13429 label: "fn sample(param1: u8, param2: u8)".to_string(),
13430 documentation: None,
13431 parameters: Some(vec![
13432 lsp::ParameterInformation {
13433 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13434 documentation: None,
13435 },
13436 lsp::ParameterInformation {
13437 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13438 documentation: None,
13439 },
13440 ]),
13441 active_parameter: None,
13442 }],
13443 active_signature: Some(0),
13444 active_parameter: Some(0),
13445 };
13446 handle_signature_help_request(&mut cx, mocked_response).await;
13447
13448 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13449 .await;
13450
13451 cx.editor(|editor, _, _| {
13452 let signature_help_state = editor.signature_help_state.popover().cloned();
13453 let signature = signature_help_state.unwrap();
13454 assert_eq!(
13455 signature.signatures[signature.current_signature].label,
13456 "fn sample(param1: u8, param2: u8)"
13457 );
13458 });
13459}
13460
13461#[gpui::test]
13462async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
13463 init_test(cx, |_| {});
13464
13465 cx.update(|cx| {
13466 cx.update_global::<SettingsStore, _>(|settings, cx| {
13467 settings.update_user_settings(cx, |settings| {
13468 settings.editor.auto_signature_help = Some(false);
13469 settings.editor.show_signature_help_after_edits = Some(false);
13470 });
13471 });
13472 });
13473
13474 let mut cx = EditorLspTestContext::new_rust(
13475 lsp::ServerCapabilities {
13476 signature_help_provider: Some(lsp::SignatureHelpOptions {
13477 ..Default::default()
13478 }),
13479 ..Default::default()
13480 },
13481 cx,
13482 )
13483 .await;
13484
13485 let language = Language::new(
13486 LanguageConfig {
13487 name: "Rust".into(),
13488 brackets: BracketPairConfig {
13489 pairs: vec![
13490 BracketPair {
13491 start: "{".to_string(),
13492 end: "}".to_string(),
13493 close: true,
13494 surround: true,
13495 newline: true,
13496 },
13497 BracketPair {
13498 start: "(".to_string(),
13499 end: ")".to_string(),
13500 close: true,
13501 surround: true,
13502 newline: true,
13503 },
13504 BracketPair {
13505 start: "/*".to_string(),
13506 end: " */".to_string(),
13507 close: true,
13508 surround: true,
13509 newline: true,
13510 },
13511 BracketPair {
13512 start: "[".to_string(),
13513 end: "]".to_string(),
13514 close: false,
13515 surround: false,
13516 newline: true,
13517 },
13518 BracketPair {
13519 start: "\"".to_string(),
13520 end: "\"".to_string(),
13521 close: true,
13522 surround: true,
13523 newline: false,
13524 },
13525 BracketPair {
13526 start: "<".to_string(),
13527 end: ">".to_string(),
13528 close: false,
13529 surround: true,
13530 newline: true,
13531 },
13532 ],
13533 ..Default::default()
13534 },
13535 autoclose_before: "})]".to_string(),
13536 ..Default::default()
13537 },
13538 Some(tree_sitter_rust::LANGUAGE.into()),
13539 );
13540 let language = Arc::new(language);
13541
13542 cx.language_registry().add(language.clone());
13543 cx.update_buffer(|buffer, cx| {
13544 buffer.set_language(Some(language), cx);
13545 });
13546
13547 // Ensure that signature_help is not called when no signature help is enabled.
13548 cx.set_state(
13549 &r#"
13550 fn main() {
13551 sampleˇ
13552 }
13553 "#
13554 .unindent(),
13555 );
13556 cx.update_editor(|editor, window, cx| {
13557 editor.handle_input("(", window, cx);
13558 });
13559 cx.assert_editor_state(
13560 &"
13561 fn main() {
13562 sample(ˇ)
13563 }
13564 "
13565 .unindent(),
13566 );
13567 cx.editor(|editor, _, _| {
13568 assert!(editor.signature_help_state.task().is_none());
13569 });
13570
13571 let mocked_response = lsp::SignatureHelp {
13572 signatures: vec![lsp::SignatureInformation {
13573 label: "fn sample(param1: u8, param2: u8)".to_string(),
13574 documentation: None,
13575 parameters: Some(vec![
13576 lsp::ParameterInformation {
13577 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13578 documentation: None,
13579 },
13580 lsp::ParameterInformation {
13581 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13582 documentation: None,
13583 },
13584 ]),
13585 active_parameter: None,
13586 }],
13587 active_signature: Some(0),
13588 active_parameter: Some(0),
13589 };
13590
13591 // Ensure that signature_help is called when enabled afte edits
13592 cx.update(|_, cx| {
13593 cx.update_global::<SettingsStore, _>(|settings, cx| {
13594 settings.update_user_settings(cx, |settings| {
13595 settings.editor.auto_signature_help = Some(false);
13596 settings.editor.show_signature_help_after_edits = Some(true);
13597 });
13598 });
13599 });
13600 cx.set_state(
13601 &r#"
13602 fn main() {
13603 sampleˇ
13604 }
13605 "#
13606 .unindent(),
13607 );
13608 cx.update_editor(|editor, window, cx| {
13609 editor.handle_input("(", window, cx);
13610 });
13611 cx.assert_editor_state(
13612 &"
13613 fn main() {
13614 sample(ˇ)
13615 }
13616 "
13617 .unindent(),
13618 );
13619 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13620 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13621 .await;
13622 cx.update_editor(|editor, _, _| {
13623 let signature_help_state = editor.signature_help_state.popover().cloned();
13624 assert!(signature_help_state.is_some());
13625 let signature = signature_help_state.unwrap();
13626 assert_eq!(
13627 signature.signatures[signature.current_signature].label,
13628 "fn sample(param1: u8, param2: u8)"
13629 );
13630 editor.signature_help_state = SignatureHelpState::default();
13631 });
13632
13633 // Ensure that signature_help is called when auto signature help override is enabled
13634 cx.update(|_, cx| {
13635 cx.update_global::<SettingsStore, _>(|settings, cx| {
13636 settings.update_user_settings(cx, |settings| {
13637 settings.editor.auto_signature_help = Some(true);
13638 settings.editor.show_signature_help_after_edits = Some(false);
13639 });
13640 });
13641 });
13642 cx.set_state(
13643 &r#"
13644 fn main() {
13645 sampleˇ
13646 }
13647 "#
13648 .unindent(),
13649 );
13650 cx.update_editor(|editor, window, cx| {
13651 editor.handle_input("(", window, cx);
13652 });
13653 cx.assert_editor_state(
13654 &"
13655 fn main() {
13656 sample(ˇ)
13657 }
13658 "
13659 .unindent(),
13660 );
13661 handle_signature_help_request(&mut cx, mocked_response).await;
13662 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13663 .await;
13664 cx.editor(|editor, _, _| {
13665 let signature_help_state = editor.signature_help_state.popover().cloned();
13666 assert!(signature_help_state.is_some());
13667 let signature = signature_help_state.unwrap();
13668 assert_eq!(
13669 signature.signatures[signature.current_signature].label,
13670 "fn sample(param1: u8, param2: u8)"
13671 );
13672 });
13673}
13674
13675#[gpui::test]
13676async fn test_signature_help(cx: &mut TestAppContext) {
13677 init_test(cx, |_| {});
13678 cx.update(|cx| {
13679 cx.update_global::<SettingsStore, _>(|settings, cx| {
13680 settings.update_user_settings(cx, |settings| {
13681 settings.editor.auto_signature_help = Some(true);
13682 });
13683 });
13684 });
13685
13686 let mut cx = EditorLspTestContext::new_rust(
13687 lsp::ServerCapabilities {
13688 signature_help_provider: Some(lsp::SignatureHelpOptions {
13689 ..Default::default()
13690 }),
13691 ..Default::default()
13692 },
13693 cx,
13694 )
13695 .await;
13696
13697 // A test that directly calls `show_signature_help`
13698 cx.update_editor(|editor, window, cx| {
13699 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13700 });
13701
13702 let mocked_response = lsp::SignatureHelp {
13703 signatures: vec![lsp::SignatureInformation {
13704 label: "fn sample(param1: u8, param2: u8)".to_string(),
13705 documentation: None,
13706 parameters: Some(vec![
13707 lsp::ParameterInformation {
13708 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13709 documentation: None,
13710 },
13711 lsp::ParameterInformation {
13712 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13713 documentation: None,
13714 },
13715 ]),
13716 active_parameter: None,
13717 }],
13718 active_signature: Some(0),
13719 active_parameter: Some(0),
13720 };
13721 handle_signature_help_request(&mut cx, mocked_response).await;
13722
13723 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13724 .await;
13725
13726 cx.editor(|editor, _, _| {
13727 let signature_help_state = editor.signature_help_state.popover().cloned();
13728 assert!(signature_help_state.is_some());
13729 let signature = signature_help_state.unwrap();
13730 assert_eq!(
13731 signature.signatures[signature.current_signature].label,
13732 "fn sample(param1: u8, param2: u8)"
13733 );
13734 });
13735
13736 // When exiting outside from inside the brackets, `signature_help` is closed.
13737 cx.set_state(indoc! {"
13738 fn main() {
13739 sample(ˇ);
13740 }
13741
13742 fn sample(param1: u8, param2: u8) {}
13743 "});
13744
13745 cx.update_editor(|editor, window, cx| {
13746 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13747 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
13748 });
13749 });
13750
13751 let mocked_response = lsp::SignatureHelp {
13752 signatures: Vec::new(),
13753 active_signature: None,
13754 active_parameter: None,
13755 };
13756 handle_signature_help_request(&mut cx, mocked_response).await;
13757
13758 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13759 .await;
13760
13761 cx.editor(|editor, _, _| {
13762 assert!(!editor.signature_help_state.is_shown());
13763 });
13764
13765 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13766 cx.set_state(indoc! {"
13767 fn main() {
13768 sample(ˇ);
13769 }
13770
13771 fn sample(param1: u8, param2: u8) {}
13772 "});
13773
13774 let mocked_response = lsp::SignatureHelp {
13775 signatures: vec![lsp::SignatureInformation {
13776 label: "fn sample(param1: u8, param2: u8)".to_string(),
13777 documentation: None,
13778 parameters: Some(vec![
13779 lsp::ParameterInformation {
13780 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13781 documentation: None,
13782 },
13783 lsp::ParameterInformation {
13784 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13785 documentation: None,
13786 },
13787 ]),
13788 active_parameter: None,
13789 }],
13790 active_signature: Some(0),
13791 active_parameter: Some(0),
13792 };
13793 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13794 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13795 .await;
13796 cx.editor(|editor, _, _| {
13797 assert!(editor.signature_help_state.is_shown());
13798 });
13799
13800 // Restore the popover with more parameter input
13801 cx.set_state(indoc! {"
13802 fn main() {
13803 sample(param1, param2ˇ);
13804 }
13805
13806 fn sample(param1: u8, param2: u8) {}
13807 "});
13808
13809 let mocked_response = lsp::SignatureHelp {
13810 signatures: vec![lsp::SignatureInformation {
13811 label: "fn sample(param1: u8, param2: u8)".to_string(),
13812 documentation: None,
13813 parameters: Some(vec![
13814 lsp::ParameterInformation {
13815 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13816 documentation: None,
13817 },
13818 lsp::ParameterInformation {
13819 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13820 documentation: None,
13821 },
13822 ]),
13823 active_parameter: None,
13824 }],
13825 active_signature: Some(0),
13826 active_parameter: Some(1),
13827 };
13828 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13829 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13830 .await;
13831
13832 // When selecting a range, the popover is gone.
13833 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13834 cx.update_editor(|editor, window, cx| {
13835 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13836 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13837 })
13838 });
13839 cx.assert_editor_state(indoc! {"
13840 fn main() {
13841 sample(param1, «ˇparam2»);
13842 }
13843
13844 fn sample(param1: u8, param2: u8) {}
13845 "});
13846 cx.editor(|editor, _, _| {
13847 assert!(!editor.signature_help_state.is_shown());
13848 });
13849
13850 // When unselecting again, the popover is back if within the brackets.
13851 cx.update_editor(|editor, window, cx| {
13852 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13853 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13854 })
13855 });
13856 cx.assert_editor_state(indoc! {"
13857 fn main() {
13858 sample(param1, ˇparam2);
13859 }
13860
13861 fn sample(param1: u8, param2: u8) {}
13862 "});
13863 handle_signature_help_request(&mut cx, mocked_response).await;
13864 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13865 .await;
13866 cx.editor(|editor, _, _| {
13867 assert!(editor.signature_help_state.is_shown());
13868 });
13869
13870 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13871 cx.update_editor(|editor, window, cx| {
13872 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13873 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13874 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13875 })
13876 });
13877 cx.assert_editor_state(indoc! {"
13878 fn main() {
13879 sample(param1, ˇparam2);
13880 }
13881
13882 fn sample(param1: u8, param2: u8) {}
13883 "});
13884
13885 let mocked_response = lsp::SignatureHelp {
13886 signatures: vec![lsp::SignatureInformation {
13887 label: "fn sample(param1: u8, param2: u8)".to_string(),
13888 documentation: None,
13889 parameters: Some(vec![
13890 lsp::ParameterInformation {
13891 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13892 documentation: None,
13893 },
13894 lsp::ParameterInformation {
13895 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13896 documentation: None,
13897 },
13898 ]),
13899 active_parameter: None,
13900 }],
13901 active_signature: Some(0),
13902 active_parameter: Some(1),
13903 };
13904 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13905 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13906 .await;
13907 cx.update_editor(|editor, _, cx| {
13908 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13909 });
13910 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13911 .await;
13912 cx.update_editor(|editor, window, cx| {
13913 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13914 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13915 })
13916 });
13917 cx.assert_editor_state(indoc! {"
13918 fn main() {
13919 sample(param1, «ˇparam2»);
13920 }
13921
13922 fn sample(param1: u8, param2: u8) {}
13923 "});
13924 cx.update_editor(|editor, window, cx| {
13925 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13926 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13927 })
13928 });
13929 cx.assert_editor_state(indoc! {"
13930 fn main() {
13931 sample(param1, ˇparam2);
13932 }
13933
13934 fn sample(param1: u8, param2: u8) {}
13935 "});
13936 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13937 .await;
13938}
13939
13940#[gpui::test]
13941async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13942 init_test(cx, |_| {});
13943
13944 let mut cx = EditorLspTestContext::new_rust(
13945 lsp::ServerCapabilities {
13946 signature_help_provider: Some(lsp::SignatureHelpOptions {
13947 ..Default::default()
13948 }),
13949 ..Default::default()
13950 },
13951 cx,
13952 )
13953 .await;
13954
13955 cx.set_state(indoc! {"
13956 fn main() {
13957 overloadedˇ
13958 }
13959 "});
13960
13961 cx.update_editor(|editor, window, cx| {
13962 editor.handle_input("(", window, cx);
13963 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13964 });
13965
13966 // Mock response with 3 signatures
13967 let mocked_response = lsp::SignatureHelp {
13968 signatures: vec![
13969 lsp::SignatureInformation {
13970 label: "fn overloaded(x: i32)".to_string(),
13971 documentation: None,
13972 parameters: Some(vec![lsp::ParameterInformation {
13973 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13974 documentation: None,
13975 }]),
13976 active_parameter: None,
13977 },
13978 lsp::SignatureInformation {
13979 label: "fn overloaded(x: i32, y: i32)".to_string(),
13980 documentation: None,
13981 parameters: Some(vec![
13982 lsp::ParameterInformation {
13983 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13984 documentation: None,
13985 },
13986 lsp::ParameterInformation {
13987 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13988 documentation: None,
13989 },
13990 ]),
13991 active_parameter: None,
13992 },
13993 lsp::SignatureInformation {
13994 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13995 documentation: None,
13996 parameters: Some(vec![
13997 lsp::ParameterInformation {
13998 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13999 documentation: None,
14000 },
14001 lsp::ParameterInformation {
14002 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
14003 documentation: None,
14004 },
14005 lsp::ParameterInformation {
14006 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
14007 documentation: None,
14008 },
14009 ]),
14010 active_parameter: None,
14011 },
14012 ],
14013 active_signature: Some(1),
14014 active_parameter: Some(0),
14015 };
14016 handle_signature_help_request(&mut cx, mocked_response).await;
14017
14018 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14019 .await;
14020
14021 // Verify we have multiple signatures and the right one is selected
14022 cx.editor(|editor, _, _| {
14023 let popover = editor.signature_help_state.popover().cloned().unwrap();
14024 assert_eq!(popover.signatures.len(), 3);
14025 // active_signature was 1, so that should be the current
14026 assert_eq!(popover.current_signature, 1);
14027 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
14028 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
14029 assert_eq!(
14030 popover.signatures[2].label,
14031 "fn overloaded(x: i32, y: i32, z: i32)"
14032 );
14033 });
14034
14035 // Test navigation functionality
14036 cx.update_editor(|editor, window, cx| {
14037 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
14038 });
14039
14040 cx.editor(|editor, _, _| {
14041 let popover = editor.signature_help_state.popover().cloned().unwrap();
14042 assert_eq!(popover.current_signature, 2);
14043 });
14044
14045 // Test wrap around
14046 cx.update_editor(|editor, window, cx| {
14047 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
14048 });
14049
14050 cx.editor(|editor, _, _| {
14051 let popover = editor.signature_help_state.popover().cloned().unwrap();
14052 assert_eq!(popover.current_signature, 0);
14053 });
14054
14055 // Test previous navigation
14056 cx.update_editor(|editor, window, cx| {
14057 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
14058 });
14059
14060 cx.editor(|editor, _, _| {
14061 let popover = editor.signature_help_state.popover().cloned().unwrap();
14062 assert_eq!(popover.current_signature, 2);
14063 });
14064}
14065
14066#[gpui::test]
14067async fn test_completion_mode(cx: &mut TestAppContext) {
14068 init_test(cx, |_| {});
14069 let mut cx = EditorLspTestContext::new_rust(
14070 lsp::ServerCapabilities {
14071 completion_provider: Some(lsp::CompletionOptions {
14072 resolve_provider: Some(true),
14073 ..Default::default()
14074 }),
14075 ..Default::default()
14076 },
14077 cx,
14078 )
14079 .await;
14080
14081 struct Run {
14082 run_description: &'static str,
14083 initial_state: String,
14084 buffer_marked_text: String,
14085 completion_label: &'static str,
14086 completion_text: &'static str,
14087 expected_with_insert_mode: String,
14088 expected_with_replace_mode: String,
14089 expected_with_replace_subsequence_mode: String,
14090 expected_with_replace_suffix_mode: String,
14091 }
14092
14093 let runs = [
14094 Run {
14095 run_description: "Start of word matches completion text",
14096 initial_state: "before ediˇ after".into(),
14097 buffer_marked_text: "before <edi|> after".into(),
14098 completion_label: "editor",
14099 completion_text: "editor",
14100 expected_with_insert_mode: "before editorˇ after".into(),
14101 expected_with_replace_mode: "before editorˇ after".into(),
14102 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14103 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14104 },
14105 Run {
14106 run_description: "Accept same text at the middle of the word",
14107 initial_state: "before ediˇtor after".into(),
14108 buffer_marked_text: "before <edi|tor> after".into(),
14109 completion_label: "editor",
14110 completion_text: "editor",
14111 expected_with_insert_mode: "before editorˇtor after".into(),
14112 expected_with_replace_mode: "before editorˇ after".into(),
14113 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14114 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14115 },
14116 Run {
14117 run_description: "End of word matches completion text -- cursor at end",
14118 initial_state: "before torˇ after".into(),
14119 buffer_marked_text: "before <tor|> after".into(),
14120 completion_label: "editor",
14121 completion_text: "editor",
14122 expected_with_insert_mode: "before editorˇ after".into(),
14123 expected_with_replace_mode: "before editorˇ after".into(),
14124 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14125 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14126 },
14127 Run {
14128 run_description: "End of word matches completion text -- cursor at start",
14129 initial_state: "before ˇtor after".into(),
14130 buffer_marked_text: "before <|tor> after".into(),
14131 completion_label: "editor",
14132 completion_text: "editor",
14133 expected_with_insert_mode: "before editorˇtor after".into(),
14134 expected_with_replace_mode: "before editorˇ after".into(),
14135 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14136 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14137 },
14138 Run {
14139 run_description: "Prepend text containing whitespace",
14140 initial_state: "pˇfield: bool".into(),
14141 buffer_marked_text: "<p|field>: bool".into(),
14142 completion_label: "pub ",
14143 completion_text: "pub ",
14144 expected_with_insert_mode: "pub ˇfield: bool".into(),
14145 expected_with_replace_mode: "pub ˇ: bool".into(),
14146 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
14147 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
14148 },
14149 Run {
14150 run_description: "Add element to start of list",
14151 initial_state: "[element_ˇelement_2]".into(),
14152 buffer_marked_text: "[<element_|element_2>]".into(),
14153 completion_label: "element_1",
14154 completion_text: "element_1",
14155 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
14156 expected_with_replace_mode: "[element_1ˇ]".into(),
14157 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
14158 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
14159 },
14160 Run {
14161 run_description: "Add element to start of list -- first and second elements are equal",
14162 initial_state: "[elˇelement]".into(),
14163 buffer_marked_text: "[<el|element>]".into(),
14164 completion_label: "element",
14165 completion_text: "element",
14166 expected_with_insert_mode: "[elementˇelement]".into(),
14167 expected_with_replace_mode: "[elementˇ]".into(),
14168 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
14169 expected_with_replace_suffix_mode: "[elementˇ]".into(),
14170 },
14171 Run {
14172 run_description: "Ends with matching suffix",
14173 initial_state: "SubˇError".into(),
14174 buffer_marked_text: "<Sub|Error>".into(),
14175 completion_label: "SubscriptionError",
14176 completion_text: "SubscriptionError",
14177 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
14178 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14179 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14180 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
14181 },
14182 Run {
14183 run_description: "Suffix is a subsequence -- contiguous",
14184 initial_state: "SubˇErr".into(),
14185 buffer_marked_text: "<Sub|Err>".into(),
14186 completion_label: "SubscriptionError",
14187 completion_text: "SubscriptionError",
14188 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
14189 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14190 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14191 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
14192 },
14193 Run {
14194 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
14195 initial_state: "Suˇscrirr".into(),
14196 buffer_marked_text: "<Su|scrirr>".into(),
14197 completion_label: "SubscriptionError",
14198 completion_text: "SubscriptionError",
14199 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
14200 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14201 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14202 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
14203 },
14204 Run {
14205 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
14206 initial_state: "foo(indˇix)".into(),
14207 buffer_marked_text: "foo(<ind|ix>)".into(),
14208 completion_label: "node_index",
14209 completion_text: "node_index",
14210 expected_with_insert_mode: "foo(node_indexˇix)".into(),
14211 expected_with_replace_mode: "foo(node_indexˇ)".into(),
14212 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
14213 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
14214 },
14215 Run {
14216 run_description: "Replace range ends before cursor - should extend to cursor",
14217 initial_state: "before editˇo after".into(),
14218 buffer_marked_text: "before <{ed}>it|o after".into(),
14219 completion_label: "editor",
14220 completion_text: "editor",
14221 expected_with_insert_mode: "before editorˇo after".into(),
14222 expected_with_replace_mode: "before editorˇo after".into(),
14223 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
14224 expected_with_replace_suffix_mode: "before editorˇo after".into(),
14225 },
14226 Run {
14227 run_description: "Uses label for suffix matching",
14228 initial_state: "before ediˇtor after".into(),
14229 buffer_marked_text: "before <edi|tor> after".into(),
14230 completion_label: "editor",
14231 completion_text: "editor()",
14232 expected_with_insert_mode: "before editor()ˇtor after".into(),
14233 expected_with_replace_mode: "before editor()ˇ after".into(),
14234 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
14235 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
14236 },
14237 Run {
14238 run_description: "Case insensitive subsequence and suffix matching",
14239 initial_state: "before EDiˇtoR after".into(),
14240 buffer_marked_text: "before <EDi|toR> after".into(),
14241 completion_label: "editor",
14242 completion_text: "editor",
14243 expected_with_insert_mode: "before editorˇtoR after".into(),
14244 expected_with_replace_mode: "before editorˇ after".into(),
14245 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14246 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14247 },
14248 ];
14249
14250 for run in runs {
14251 let run_variations = [
14252 (LspInsertMode::Insert, run.expected_with_insert_mode),
14253 (LspInsertMode::Replace, run.expected_with_replace_mode),
14254 (
14255 LspInsertMode::ReplaceSubsequence,
14256 run.expected_with_replace_subsequence_mode,
14257 ),
14258 (
14259 LspInsertMode::ReplaceSuffix,
14260 run.expected_with_replace_suffix_mode,
14261 ),
14262 ];
14263
14264 for (lsp_insert_mode, expected_text) in run_variations {
14265 eprintln!(
14266 "run = {:?}, mode = {lsp_insert_mode:.?}",
14267 run.run_description,
14268 );
14269
14270 update_test_language_settings(&mut cx, |settings| {
14271 settings.defaults.completions = Some(CompletionSettingsContent {
14272 lsp_insert_mode: Some(lsp_insert_mode),
14273 words: Some(WordsCompletionMode::Disabled),
14274 words_min_length: Some(0),
14275 ..Default::default()
14276 });
14277 });
14278
14279 cx.set_state(&run.initial_state);
14280 cx.update_editor(|editor, window, cx| {
14281 editor.show_completions(&ShowCompletions, window, cx);
14282 });
14283
14284 let counter = Arc::new(AtomicUsize::new(0));
14285 handle_completion_request_with_insert_and_replace(
14286 &mut cx,
14287 &run.buffer_marked_text,
14288 vec![(run.completion_label, run.completion_text)],
14289 counter.clone(),
14290 )
14291 .await;
14292 cx.condition(|editor, _| editor.context_menu_visible())
14293 .await;
14294 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14295
14296 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14297 editor
14298 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14299 .unwrap()
14300 });
14301 cx.assert_editor_state(&expected_text);
14302 handle_resolve_completion_request(&mut cx, None).await;
14303 apply_additional_edits.await.unwrap();
14304 }
14305 }
14306}
14307
14308#[gpui::test]
14309async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
14310 init_test(cx, |_| {});
14311 let mut cx = EditorLspTestContext::new_rust(
14312 lsp::ServerCapabilities {
14313 completion_provider: Some(lsp::CompletionOptions {
14314 resolve_provider: Some(true),
14315 ..Default::default()
14316 }),
14317 ..Default::default()
14318 },
14319 cx,
14320 )
14321 .await;
14322
14323 let initial_state = "SubˇError";
14324 let buffer_marked_text = "<Sub|Error>";
14325 let completion_text = "SubscriptionError";
14326 let expected_with_insert_mode = "SubscriptionErrorˇError";
14327 let expected_with_replace_mode = "SubscriptionErrorˇ";
14328
14329 update_test_language_settings(&mut cx, |settings| {
14330 settings.defaults.completions = Some(CompletionSettingsContent {
14331 words: Some(WordsCompletionMode::Disabled),
14332 words_min_length: Some(0),
14333 // set the opposite here to ensure that the action is overriding the default behavior
14334 lsp_insert_mode: Some(LspInsertMode::Insert),
14335 ..Default::default()
14336 });
14337 });
14338
14339 cx.set_state(initial_state);
14340 cx.update_editor(|editor, window, cx| {
14341 editor.show_completions(&ShowCompletions, window, cx);
14342 });
14343
14344 let counter = Arc::new(AtomicUsize::new(0));
14345 handle_completion_request_with_insert_and_replace(
14346 &mut cx,
14347 buffer_marked_text,
14348 vec![(completion_text, completion_text)],
14349 counter.clone(),
14350 )
14351 .await;
14352 cx.condition(|editor, _| editor.context_menu_visible())
14353 .await;
14354 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14355
14356 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14357 editor
14358 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14359 .unwrap()
14360 });
14361 cx.assert_editor_state(expected_with_replace_mode);
14362 handle_resolve_completion_request(&mut cx, None).await;
14363 apply_additional_edits.await.unwrap();
14364
14365 update_test_language_settings(&mut cx, |settings| {
14366 settings.defaults.completions = Some(CompletionSettingsContent {
14367 words: Some(WordsCompletionMode::Disabled),
14368 words_min_length: Some(0),
14369 // set the opposite here to ensure that the action is overriding the default behavior
14370 lsp_insert_mode: Some(LspInsertMode::Replace),
14371 ..Default::default()
14372 });
14373 });
14374
14375 cx.set_state(initial_state);
14376 cx.update_editor(|editor, window, cx| {
14377 editor.show_completions(&ShowCompletions, window, cx);
14378 });
14379 handle_completion_request_with_insert_and_replace(
14380 &mut cx,
14381 buffer_marked_text,
14382 vec![(completion_text, completion_text)],
14383 counter.clone(),
14384 )
14385 .await;
14386 cx.condition(|editor, _| editor.context_menu_visible())
14387 .await;
14388 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14389
14390 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14391 editor
14392 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
14393 .unwrap()
14394 });
14395 cx.assert_editor_state(expected_with_insert_mode);
14396 handle_resolve_completion_request(&mut cx, None).await;
14397 apply_additional_edits.await.unwrap();
14398}
14399
14400#[gpui::test]
14401async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
14402 init_test(cx, |_| {});
14403 let mut cx = EditorLspTestContext::new_rust(
14404 lsp::ServerCapabilities {
14405 completion_provider: Some(lsp::CompletionOptions {
14406 resolve_provider: Some(true),
14407 ..Default::default()
14408 }),
14409 ..Default::default()
14410 },
14411 cx,
14412 )
14413 .await;
14414
14415 // scenario: surrounding text matches completion text
14416 let completion_text = "to_offset";
14417 let initial_state = indoc! {"
14418 1. buf.to_offˇsuffix
14419 2. buf.to_offˇsuf
14420 3. buf.to_offˇfix
14421 4. buf.to_offˇ
14422 5. into_offˇensive
14423 6. ˇsuffix
14424 7. let ˇ //
14425 8. aaˇzz
14426 9. buf.to_off«zzzzzˇ»suffix
14427 10. buf.«ˇzzzzz»suffix
14428 11. to_off«ˇzzzzz»
14429
14430 buf.to_offˇsuffix // newest cursor
14431 "};
14432 let completion_marked_buffer = indoc! {"
14433 1. buf.to_offsuffix
14434 2. buf.to_offsuf
14435 3. buf.to_offfix
14436 4. buf.to_off
14437 5. into_offensive
14438 6. suffix
14439 7. let //
14440 8. aazz
14441 9. buf.to_offzzzzzsuffix
14442 10. buf.zzzzzsuffix
14443 11. to_offzzzzz
14444
14445 buf.<to_off|suffix> // newest cursor
14446 "};
14447 let expected = indoc! {"
14448 1. buf.to_offsetˇ
14449 2. buf.to_offsetˇsuf
14450 3. buf.to_offsetˇfix
14451 4. buf.to_offsetˇ
14452 5. into_offsetˇensive
14453 6. to_offsetˇsuffix
14454 7. let to_offsetˇ //
14455 8. aato_offsetˇzz
14456 9. buf.to_offsetˇ
14457 10. buf.to_offsetˇsuffix
14458 11. to_offsetˇ
14459
14460 buf.to_offsetˇ // newest cursor
14461 "};
14462 cx.set_state(initial_state);
14463 cx.update_editor(|editor, window, cx| {
14464 editor.show_completions(&ShowCompletions, window, cx);
14465 });
14466 handle_completion_request_with_insert_and_replace(
14467 &mut cx,
14468 completion_marked_buffer,
14469 vec![(completion_text, completion_text)],
14470 Arc::new(AtomicUsize::new(0)),
14471 )
14472 .await;
14473 cx.condition(|editor, _| editor.context_menu_visible())
14474 .await;
14475 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14476 editor
14477 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14478 .unwrap()
14479 });
14480 cx.assert_editor_state(expected);
14481 handle_resolve_completion_request(&mut cx, None).await;
14482 apply_additional_edits.await.unwrap();
14483
14484 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14485 let completion_text = "foo_and_bar";
14486 let initial_state = indoc! {"
14487 1. ooanbˇ
14488 2. zooanbˇ
14489 3. ooanbˇz
14490 4. zooanbˇz
14491 5. ooanˇ
14492 6. oanbˇ
14493
14494 ooanbˇ
14495 "};
14496 let completion_marked_buffer = indoc! {"
14497 1. ooanb
14498 2. zooanb
14499 3. ooanbz
14500 4. zooanbz
14501 5. ooan
14502 6. oanb
14503
14504 <ooanb|>
14505 "};
14506 let expected = indoc! {"
14507 1. foo_and_barˇ
14508 2. zfoo_and_barˇ
14509 3. foo_and_barˇz
14510 4. zfoo_and_barˇz
14511 5. ooanfoo_and_barˇ
14512 6. oanbfoo_and_barˇ
14513
14514 foo_and_barˇ
14515 "};
14516 cx.set_state(initial_state);
14517 cx.update_editor(|editor, window, cx| {
14518 editor.show_completions(&ShowCompletions, window, cx);
14519 });
14520 handle_completion_request_with_insert_and_replace(
14521 &mut cx,
14522 completion_marked_buffer,
14523 vec![(completion_text, completion_text)],
14524 Arc::new(AtomicUsize::new(0)),
14525 )
14526 .await;
14527 cx.condition(|editor, _| editor.context_menu_visible())
14528 .await;
14529 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14530 editor
14531 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14532 .unwrap()
14533 });
14534 cx.assert_editor_state(expected);
14535 handle_resolve_completion_request(&mut cx, None).await;
14536 apply_additional_edits.await.unwrap();
14537
14538 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14539 // (expects the same as if it was inserted at the end)
14540 let completion_text = "foo_and_bar";
14541 let initial_state = indoc! {"
14542 1. ooˇanb
14543 2. zooˇanb
14544 3. ooˇanbz
14545 4. zooˇanbz
14546
14547 ooˇanb
14548 "};
14549 let completion_marked_buffer = indoc! {"
14550 1. ooanb
14551 2. zooanb
14552 3. ooanbz
14553 4. zooanbz
14554
14555 <oo|anb>
14556 "};
14557 let expected = indoc! {"
14558 1. foo_and_barˇ
14559 2. zfoo_and_barˇ
14560 3. foo_and_barˇz
14561 4. zfoo_and_barˇz
14562
14563 foo_and_barˇ
14564 "};
14565 cx.set_state(initial_state);
14566 cx.update_editor(|editor, window, cx| {
14567 editor.show_completions(&ShowCompletions, window, cx);
14568 });
14569 handle_completion_request_with_insert_and_replace(
14570 &mut cx,
14571 completion_marked_buffer,
14572 vec![(completion_text, completion_text)],
14573 Arc::new(AtomicUsize::new(0)),
14574 )
14575 .await;
14576 cx.condition(|editor, _| editor.context_menu_visible())
14577 .await;
14578 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14579 editor
14580 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14581 .unwrap()
14582 });
14583 cx.assert_editor_state(expected);
14584 handle_resolve_completion_request(&mut cx, None).await;
14585 apply_additional_edits.await.unwrap();
14586}
14587
14588// This used to crash
14589#[gpui::test]
14590async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14591 init_test(cx, |_| {});
14592
14593 let buffer_text = indoc! {"
14594 fn main() {
14595 10.satu;
14596
14597 //
14598 // separate cursors so they open in different excerpts (manually reproducible)
14599 //
14600
14601 10.satu20;
14602 }
14603 "};
14604 let multibuffer_text_with_selections = indoc! {"
14605 fn main() {
14606 10.satuˇ;
14607
14608 //
14609
14610 //
14611
14612 10.satuˇ20;
14613 }
14614 "};
14615 let expected_multibuffer = indoc! {"
14616 fn main() {
14617 10.saturating_sub()ˇ;
14618
14619 //
14620
14621 //
14622
14623 10.saturating_sub()ˇ;
14624 }
14625 "};
14626
14627 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14628 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14629
14630 let fs = FakeFs::new(cx.executor());
14631 fs.insert_tree(
14632 path!("/a"),
14633 json!({
14634 "main.rs": buffer_text,
14635 }),
14636 )
14637 .await;
14638
14639 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14640 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14641 language_registry.add(rust_lang());
14642 let mut fake_servers = language_registry.register_fake_lsp(
14643 "Rust",
14644 FakeLspAdapter {
14645 capabilities: lsp::ServerCapabilities {
14646 completion_provider: Some(lsp::CompletionOptions {
14647 resolve_provider: None,
14648 ..lsp::CompletionOptions::default()
14649 }),
14650 ..lsp::ServerCapabilities::default()
14651 },
14652 ..FakeLspAdapter::default()
14653 },
14654 );
14655 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14656 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14657 let buffer = project
14658 .update(cx, |project, cx| {
14659 project.open_local_buffer(path!("/a/main.rs"), cx)
14660 })
14661 .await
14662 .unwrap();
14663
14664 let multi_buffer = cx.new(|cx| {
14665 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14666 multi_buffer.push_excerpts(
14667 buffer.clone(),
14668 [ExcerptRange::new(0..first_excerpt_end)],
14669 cx,
14670 );
14671 multi_buffer.push_excerpts(
14672 buffer.clone(),
14673 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14674 cx,
14675 );
14676 multi_buffer
14677 });
14678
14679 let editor = workspace
14680 .update(cx, |_, window, cx| {
14681 cx.new(|cx| {
14682 Editor::new(
14683 EditorMode::Full {
14684 scale_ui_elements_with_buffer_font_size: false,
14685 show_active_line_background: false,
14686 sizing_behavior: SizingBehavior::Default,
14687 },
14688 multi_buffer.clone(),
14689 Some(project.clone()),
14690 window,
14691 cx,
14692 )
14693 })
14694 })
14695 .unwrap();
14696
14697 let pane = workspace
14698 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14699 .unwrap();
14700 pane.update_in(cx, |pane, window, cx| {
14701 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14702 });
14703
14704 let fake_server = fake_servers.next().await.unwrap();
14705
14706 editor.update_in(cx, |editor, window, cx| {
14707 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14708 s.select_ranges([
14709 Point::new(1, 11)..Point::new(1, 11),
14710 Point::new(7, 11)..Point::new(7, 11),
14711 ])
14712 });
14713
14714 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14715 });
14716
14717 editor.update_in(cx, |editor, window, cx| {
14718 editor.show_completions(&ShowCompletions, window, cx);
14719 });
14720
14721 fake_server
14722 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14723 let completion_item = lsp::CompletionItem {
14724 label: "saturating_sub()".into(),
14725 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14726 lsp::InsertReplaceEdit {
14727 new_text: "saturating_sub()".to_owned(),
14728 insert: lsp::Range::new(
14729 lsp::Position::new(7, 7),
14730 lsp::Position::new(7, 11),
14731 ),
14732 replace: lsp::Range::new(
14733 lsp::Position::new(7, 7),
14734 lsp::Position::new(7, 13),
14735 ),
14736 },
14737 )),
14738 ..lsp::CompletionItem::default()
14739 };
14740
14741 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14742 })
14743 .next()
14744 .await
14745 .unwrap();
14746
14747 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14748 .await;
14749
14750 editor
14751 .update_in(cx, |editor, window, cx| {
14752 editor
14753 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14754 .unwrap()
14755 })
14756 .await
14757 .unwrap();
14758
14759 editor.update(cx, |editor, cx| {
14760 assert_text_with_selections(editor, expected_multibuffer, cx);
14761 })
14762}
14763
14764#[gpui::test]
14765async fn test_completion(cx: &mut TestAppContext) {
14766 init_test(cx, |_| {});
14767
14768 let mut cx = EditorLspTestContext::new_rust(
14769 lsp::ServerCapabilities {
14770 completion_provider: Some(lsp::CompletionOptions {
14771 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14772 resolve_provider: Some(true),
14773 ..Default::default()
14774 }),
14775 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14776 ..Default::default()
14777 },
14778 cx,
14779 )
14780 .await;
14781 let counter = Arc::new(AtomicUsize::new(0));
14782
14783 cx.set_state(indoc! {"
14784 oneˇ
14785 two
14786 three
14787 "});
14788 cx.simulate_keystroke(".");
14789 handle_completion_request(
14790 indoc! {"
14791 one.|<>
14792 two
14793 three
14794 "},
14795 vec!["first_completion", "second_completion"],
14796 true,
14797 counter.clone(),
14798 &mut cx,
14799 )
14800 .await;
14801 cx.condition(|editor, _| editor.context_menu_visible())
14802 .await;
14803 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14804
14805 let _handler = handle_signature_help_request(
14806 &mut cx,
14807 lsp::SignatureHelp {
14808 signatures: vec![lsp::SignatureInformation {
14809 label: "test signature".to_string(),
14810 documentation: None,
14811 parameters: Some(vec![lsp::ParameterInformation {
14812 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14813 documentation: None,
14814 }]),
14815 active_parameter: None,
14816 }],
14817 active_signature: None,
14818 active_parameter: None,
14819 },
14820 );
14821 cx.update_editor(|editor, window, cx| {
14822 assert!(
14823 !editor.signature_help_state.is_shown(),
14824 "No signature help was called for"
14825 );
14826 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14827 });
14828 cx.run_until_parked();
14829 cx.update_editor(|editor, _, _| {
14830 assert!(
14831 !editor.signature_help_state.is_shown(),
14832 "No signature help should be shown when completions menu is open"
14833 );
14834 });
14835
14836 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14837 editor.context_menu_next(&Default::default(), window, cx);
14838 editor
14839 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14840 .unwrap()
14841 });
14842 cx.assert_editor_state(indoc! {"
14843 one.second_completionˇ
14844 two
14845 three
14846 "});
14847
14848 handle_resolve_completion_request(
14849 &mut cx,
14850 Some(vec![
14851 (
14852 //This overlaps with the primary completion edit which is
14853 //misbehavior from the LSP spec, test that we filter it out
14854 indoc! {"
14855 one.second_ˇcompletion
14856 two
14857 threeˇ
14858 "},
14859 "overlapping additional edit",
14860 ),
14861 (
14862 indoc! {"
14863 one.second_completion
14864 two
14865 threeˇ
14866 "},
14867 "\nadditional edit",
14868 ),
14869 ]),
14870 )
14871 .await;
14872 apply_additional_edits.await.unwrap();
14873 cx.assert_editor_state(indoc! {"
14874 one.second_completionˇ
14875 two
14876 three
14877 additional edit
14878 "});
14879
14880 cx.set_state(indoc! {"
14881 one.second_completion
14882 twoˇ
14883 threeˇ
14884 additional edit
14885 "});
14886 cx.simulate_keystroke(" ");
14887 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14888 cx.simulate_keystroke("s");
14889 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14890
14891 cx.assert_editor_state(indoc! {"
14892 one.second_completion
14893 two sˇ
14894 three sˇ
14895 additional edit
14896 "});
14897 handle_completion_request(
14898 indoc! {"
14899 one.second_completion
14900 two s
14901 three <s|>
14902 additional edit
14903 "},
14904 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14905 true,
14906 counter.clone(),
14907 &mut cx,
14908 )
14909 .await;
14910 cx.condition(|editor, _| editor.context_menu_visible())
14911 .await;
14912 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14913
14914 cx.simulate_keystroke("i");
14915
14916 handle_completion_request(
14917 indoc! {"
14918 one.second_completion
14919 two si
14920 three <si|>
14921 additional edit
14922 "},
14923 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14924 true,
14925 counter.clone(),
14926 &mut cx,
14927 )
14928 .await;
14929 cx.condition(|editor, _| editor.context_menu_visible())
14930 .await;
14931 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14932
14933 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14934 editor
14935 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14936 .unwrap()
14937 });
14938 cx.assert_editor_state(indoc! {"
14939 one.second_completion
14940 two sixth_completionˇ
14941 three sixth_completionˇ
14942 additional edit
14943 "});
14944
14945 apply_additional_edits.await.unwrap();
14946
14947 update_test_language_settings(&mut cx, |settings| {
14948 settings.defaults.show_completions_on_input = Some(false);
14949 });
14950 cx.set_state("editorˇ");
14951 cx.simulate_keystroke(".");
14952 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14953 cx.simulate_keystrokes("c l o");
14954 cx.assert_editor_state("editor.cloˇ");
14955 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14956 cx.update_editor(|editor, window, cx| {
14957 editor.show_completions(&ShowCompletions, window, cx);
14958 });
14959 handle_completion_request(
14960 "editor.<clo|>",
14961 vec!["close", "clobber"],
14962 true,
14963 counter.clone(),
14964 &mut cx,
14965 )
14966 .await;
14967 cx.condition(|editor, _| editor.context_menu_visible())
14968 .await;
14969 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14970
14971 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14972 editor
14973 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14974 .unwrap()
14975 });
14976 cx.assert_editor_state("editor.clobberˇ");
14977 handle_resolve_completion_request(&mut cx, None).await;
14978 apply_additional_edits.await.unwrap();
14979}
14980
14981#[gpui::test]
14982async fn test_completion_can_run_commands(cx: &mut TestAppContext) {
14983 init_test(cx, |_| {});
14984
14985 let fs = FakeFs::new(cx.executor());
14986 fs.insert_tree(
14987 path!("/a"),
14988 json!({
14989 "main.rs": "",
14990 }),
14991 )
14992 .await;
14993
14994 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14995 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14996 language_registry.add(rust_lang());
14997 let command_calls = Arc::new(AtomicUsize::new(0));
14998 let registered_command = "_the/command";
14999
15000 let closure_command_calls = command_calls.clone();
15001 let mut fake_servers = language_registry.register_fake_lsp(
15002 "Rust",
15003 FakeLspAdapter {
15004 capabilities: lsp::ServerCapabilities {
15005 completion_provider: Some(lsp::CompletionOptions {
15006 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15007 ..lsp::CompletionOptions::default()
15008 }),
15009 execute_command_provider: Some(lsp::ExecuteCommandOptions {
15010 commands: vec![registered_command.to_owned()],
15011 ..lsp::ExecuteCommandOptions::default()
15012 }),
15013 ..lsp::ServerCapabilities::default()
15014 },
15015 initializer: Some(Box::new(move |fake_server| {
15016 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15017 move |params, _| async move {
15018 Ok(Some(lsp::CompletionResponse::Array(vec![
15019 lsp::CompletionItem {
15020 label: "registered_command".to_owned(),
15021 text_edit: gen_text_edit(¶ms, ""),
15022 command: Some(lsp::Command {
15023 title: registered_command.to_owned(),
15024 command: "_the/command".to_owned(),
15025 arguments: Some(vec![serde_json::Value::Bool(true)]),
15026 }),
15027 ..lsp::CompletionItem::default()
15028 },
15029 lsp::CompletionItem {
15030 label: "unregistered_command".to_owned(),
15031 text_edit: gen_text_edit(¶ms, ""),
15032 command: Some(lsp::Command {
15033 title: "????????????".to_owned(),
15034 command: "????????????".to_owned(),
15035 arguments: Some(vec![serde_json::Value::Null]),
15036 }),
15037 ..lsp::CompletionItem::default()
15038 },
15039 ])))
15040 },
15041 );
15042 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
15043 let command_calls = closure_command_calls.clone();
15044 move |params, _| {
15045 assert_eq!(params.command, registered_command);
15046 let command_calls = command_calls.clone();
15047 async move {
15048 command_calls.fetch_add(1, atomic::Ordering::Release);
15049 Ok(Some(json!(null)))
15050 }
15051 }
15052 });
15053 })),
15054 ..FakeLspAdapter::default()
15055 },
15056 );
15057 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15058 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15059 let editor = workspace
15060 .update(cx, |workspace, window, cx| {
15061 workspace.open_abs_path(
15062 PathBuf::from(path!("/a/main.rs")),
15063 OpenOptions::default(),
15064 window,
15065 cx,
15066 )
15067 })
15068 .unwrap()
15069 .await
15070 .unwrap()
15071 .downcast::<Editor>()
15072 .unwrap();
15073 let _fake_server = fake_servers.next().await.unwrap();
15074
15075 editor.update_in(cx, |editor, window, cx| {
15076 cx.focus_self(window);
15077 editor.move_to_end(&MoveToEnd, window, cx);
15078 editor.handle_input(".", window, cx);
15079 });
15080 cx.run_until_parked();
15081 editor.update(cx, |editor, _| {
15082 assert!(editor.context_menu_visible());
15083 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15084 {
15085 let completion_labels = menu
15086 .completions
15087 .borrow()
15088 .iter()
15089 .map(|c| c.label.text.clone())
15090 .collect::<Vec<_>>();
15091 assert_eq!(
15092 completion_labels,
15093 &["registered_command", "unregistered_command",],
15094 );
15095 } else {
15096 panic!("expected completion menu to be open");
15097 }
15098 });
15099
15100 editor
15101 .update_in(cx, |editor, window, cx| {
15102 editor
15103 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15104 .unwrap()
15105 })
15106 .await
15107 .unwrap();
15108 cx.run_until_parked();
15109 assert_eq!(
15110 command_calls.load(atomic::Ordering::Acquire),
15111 1,
15112 "For completion with a registered command, Zed should send a command execution request",
15113 );
15114
15115 editor.update_in(cx, |editor, window, cx| {
15116 cx.focus_self(window);
15117 editor.handle_input(".", window, cx);
15118 });
15119 cx.run_until_parked();
15120 editor.update(cx, |editor, _| {
15121 assert!(editor.context_menu_visible());
15122 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15123 {
15124 let completion_labels = menu
15125 .completions
15126 .borrow()
15127 .iter()
15128 .map(|c| c.label.text.clone())
15129 .collect::<Vec<_>>();
15130 assert_eq!(
15131 completion_labels,
15132 &["registered_command", "unregistered_command",],
15133 );
15134 } else {
15135 panic!("expected completion menu to be open");
15136 }
15137 });
15138 editor
15139 .update_in(cx, |editor, window, cx| {
15140 editor.context_menu_next(&Default::default(), window, cx);
15141 editor
15142 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15143 .unwrap()
15144 })
15145 .await
15146 .unwrap();
15147 cx.run_until_parked();
15148 assert_eq!(
15149 command_calls.load(atomic::Ordering::Acquire),
15150 1,
15151 "For completion with an unregistered command, Zed should not send a command execution request",
15152 );
15153}
15154
15155#[gpui::test]
15156async fn test_completion_reuse(cx: &mut TestAppContext) {
15157 init_test(cx, |_| {});
15158
15159 let mut cx = EditorLspTestContext::new_rust(
15160 lsp::ServerCapabilities {
15161 completion_provider: Some(lsp::CompletionOptions {
15162 trigger_characters: Some(vec![".".to_string()]),
15163 ..Default::default()
15164 }),
15165 ..Default::default()
15166 },
15167 cx,
15168 )
15169 .await;
15170
15171 let counter = Arc::new(AtomicUsize::new(0));
15172 cx.set_state("objˇ");
15173 cx.simulate_keystroke(".");
15174
15175 // Initial completion request returns complete results
15176 let is_incomplete = false;
15177 handle_completion_request(
15178 "obj.|<>",
15179 vec!["a", "ab", "abc"],
15180 is_incomplete,
15181 counter.clone(),
15182 &mut cx,
15183 )
15184 .await;
15185 cx.run_until_parked();
15186 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15187 cx.assert_editor_state("obj.ˇ");
15188 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15189
15190 // Type "a" - filters existing completions
15191 cx.simulate_keystroke("a");
15192 cx.run_until_parked();
15193 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15194 cx.assert_editor_state("obj.aˇ");
15195 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15196
15197 // Type "b" - filters existing completions
15198 cx.simulate_keystroke("b");
15199 cx.run_until_parked();
15200 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15201 cx.assert_editor_state("obj.abˇ");
15202 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15203
15204 // Type "c" - filters existing completions
15205 cx.simulate_keystroke("c");
15206 cx.run_until_parked();
15207 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15208 cx.assert_editor_state("obj.abcˇ");
15209 check_displayed_completions(vec!["abc"], &mut cx);
15210
15211 // Backspace to delete "c" - filters existing completions
15212 cx.update_editor(|editor, window, cx| {
15213 editor.backspace(&Backspace, window, cx);
15214 });
15215 cx.run_until_parked();
15216 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15217 cx.assert_editor_state("obj.abˇ");
15218 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15219
15220 // Moving cursor to the left dismisses menu.
15221 cx.update_editor(|editor, window, cx| {
15222 editor.move_left(&MoveLeft, window, cx);
15223 });
15224 cx.run_until_parked();
15225 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15226 cx.assert_editor_state("obj.aˇb");
15227 cx.update_editor(|editor, _, _| {
15228 assert_eq!(editor.context_menu_visible(), false);
15229 });
15230
15231 // Type "b" - new request
15232 cx.simulate_keystroke("b");
15233 let is_incomplete = false;
15234 handle_completion_request(
15235 "obj.<ab|>a",
15236 vec!["ab", "abc"],
15237 is_incomplete,
15238 counter.clone(),
15239 &mut cx,
15240 )
15241 .await;
15242 cx.run_until_parked();
15243 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
15244 cx.assert_editor_state("obj.abˇb");
15245 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15246
15247 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
15248 cx.update_editor(|editor, window, cx| {
15249 editor.backspace(&Backspace, window, cx);
15250 });
15251 let is_incomplete = false;
15252 handle_completion_request(
15253 "obj.<a|>b",
15254 vec!["a", "ab", "abc"],
15255 is_incomplete,
15256 counter.clone(),
15257 &mut cx,
15258 )
15259 .await;
15260 cx.run_until_parked();
15261 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15262 cx.assert_editor_state("obj.aˇb");
15263 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15264
15265 // Backspace to delete "a" - dismisses menu.
15266 cx.update_editor(|editor, window, cx| {
15267 editor.backspace(&Backspace, window, cx);
15268 });
15269 cx.run_until_parked();
15270 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15271 cx.assert_editor_state("obj.ˇb");
15272 cx.update_editor(|editor, _, _| {
15273 assert_eq!(editor.context_menu_visible(), false);
15274 });
15275}
15276
15277#[gpui::test]
15278async fn test_word_completion(cx: &mut TestAppContext) {
15279 let lsp_fetch_timeout_ms = 10;
15280 init_test(cx, |language_settings| {
15281 language_settings.defaults.completions = Some(CompletionSettingsContent {
15282 words_min_length: Some(0),
15283 lsp_fetch_timeout_ms: Some(10),
15284 lsp_insert_mode: Some(LspInsertMode::Insert),
15285 ..Default::default()
15286 });
15287 });
15288
15289 let mut cx = EditorLspTestContext::new_rust(
15290 lsp::ServerCapabilities {
15291 completion_provider: Some(lsp::CompletionOptions {
15292 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15293 ..lsp::CompletionOptions::default()
15294 }),
15295 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15296 ..lsp::ServerCapabilities::default()
15297 },
15298 cx,
15299 )
15300 .await;
15301
15302 let throttle_completions = Arc::new(AtomicBool::new(false));
15303
15304 let lsp_throttle_completions = throttle_completions.clone();
15305 let _completion_requests_handler =
15306 cx.lsp
15307 .server
15308 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
15309 let lsp_throttle_completions = lsp_throttle_completions.clone();
15310 let cx = cx.clone();
15311 async move {
15312 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
15313 cx.background_executor()
15314 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
15315 .await;
15316 }
15317 Ok(Some(lsp::CompletionResponse::Array(vec![
15318 lsp::CompletionItem {
15319 label: "first".into(),
15320 ..lsp::CompletionItem::default()
15321 },
15322 lsp::CompletionItem {
15323 label: "last".into(),
15324 ..lsp::CompletionItem::default()
15325 },
15326 ])))
15327 }
15328 });
15329
15330 cx.set_state(indoc! {"
15331 oneˇ
15332 two
15333 three
15334 "});
15335 cx.simulate_keystroke(".");
15336 cx.executor().run_until_parked();
15337 cx.condition(|editor, _| editor.context_menu_visible())
15338 .await;
15339 cx.update_editor(|editor, window, cx| {
15340 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15341 {
15342 assert_eq!(
15343 completion_menu_entries(menu),
15344 &["first", "last"],
15345 "When LSP server is fast to reply, no fallback word completions are used"
15346 );
15347 } else {
15348 panic!("expected completion menu to be open");
15349 }
15350 editor.cancel(&Cancel, window, cx);
15351 });
15352 cx.executor().run_until_parked();
15353 cx.condition(|editor, _| !editor.context_menu_visible())
15354 .await;
15355
15356 throttle_completions.store(true, atomic::Ordering::Release);
15357 cx.simulate_keystroke(".");
15358 cx.executor()
15359 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
15360 cx.executor().run_until_parked();
15361 cx.condition(|editor, _| editor.context_menu_visible())
15362 .await;
15363 cx.update_editor(|editor, _, _| {
15364 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15365 {
15366 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
15367 "When LSP server is slow, document words can be shown instead, if configured accordingly");
15368 } else {
15369 panic!("expected completion menu to be open");
15370 }
15371 });
15372}
15373
15374#[gpui::test]
15375async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
15376 init_test(cx, |language_settings| {
15377 language_settings.defaults.completions = Some(CompletionSettingsContent {
15378 words: Some(WordsCompletionMode::Enabled),
15379 words_min_length: Some(0),
15380 lsp_insert_mode: Some(LspInsertMode::Insert),
15381 ..Default::default()
15382 });
15383 });
15384
15385 let mut cx = EditorLspTestContext::new_rust(
15386 lsp::ServerCapabilities {
15387 completion_provider: Some(lsp::CompletionOptions {
15388 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15389 ..lsp::CompletionOptions::default()
15390 }),
15391 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15392 ..lsp::ServerCapabilities::default()
15393 },
15394 cx,
15395 )
15396 .await;
15397
15398 let _completion_requests_handler =
15399 cx.lsp
15400 .server
15401 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15402 Ok(Some(lsp::CompletionResponse::Array(vec![
15403 lsp::CompletionItem {
15404 label: "first".into(),
15405 ..lsp::CompletionItem::default()
15406 },
15407 lsp::CompletionItem {
15408 label: "last".into(),
15409 ..lsp::CompletionItem::default()
15410 },
15411 ])))
15412 });
15413
15414 cx.set_state(indoc! {"ˇ
15415 first
15416 last
15417 second
15418 "});
15419 cx.simulate_keystroke(".");
15420 cx.executor().run_until_parked();
15421 cx.condition(|editor, _| editor.context_menu_visible())
15422 .await;
15423 cx.update_editor(|editor, _, _| {
15424 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15425 {
15426 assert_eq!(
15427 completion_menu_entries(menu),
15428 &["first", "last", "second"],
15429 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
15430 );
15431 } else {
15432 panic!("expected completion menu to be open");
15433 }
15434 });
15435}
15436
15437#[gpui::test]
15438async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
15439 init_test(cx, |language_settings| {
15440 language_settings.defaults.completions = Some(CompletionSettingsContent {
15441 words: Some(WordsCompletionMode::Disabled),
15442 words_min_length: Some(0),
15443 lsp_insert_mode: Some(LspInsertMode::Insert),
15444 ..Default::default()
15445 });
15446 });
15447
15448 let mut cx = EditorLspTestContext::new_rust(
15449 lsp::ServerCapabilities {
15450 completion_provider: Some(lsp::CompletionOptions {
15451 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15452 ..lsp::CompletionOptions::default()
15453 }),
15454 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15455 ..lsp::ServerCapabilities::default()
15456 },
15457 cx,
15458 )
15459 .await;
15460
15461 let _completion_requests_handler =
15462 cx.lsp
15463 .server
15464 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15465 panic!("LSP completions should not be queried when dealing with word completions")
15466 });
15467
15468 cx.set_state(indoc! {"ˇ
15469 first
15470 last
15471 second
15472 "});
15473 cx.update_editor(|editor, window, cx| {
15474 editor.show_word_completions(&ShowWordCompletions, window, cx);
15475 });
15476 cx.executor().run_until_parked();
15477 cx.condition(|editor, _| editor.context_menu_visible())
15478 .await;
15479 cx.update_editor(|editor, _, _| {
15480 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15481 {
15482 assert_eq!(
15483 completion_menu_entries(menu),
15484 &["first", "last", "second"],
15485 "`ShowWordCompletions` action should show word completions"
15486 );
15487 } else {
15488 panic!("expected completion menu to be open");
15489 }
15490 });
15491
15492 cx.simulate_keystroke("l");
15493 cx.executor().run_until_parked();
15494 cx.condition(|editor, _| editor.context_menu_visible())
15495 .await;
15496 cx.update_editor(|editor, _, _| {
15497 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15498 {
15499 assert_eq!(
15500 completion_menu_entries(menu),
15501 &["last"],
15502 "After showing word completions, further editing should filter them and not query the LSP"
15503 );
15504 } else {
15505 panic!("expected completion menu to be open");
15506 }
15507 });
15508}
15509
15510#[gpui::test]
15511async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
15512 init_test(cx, |language_settings| {
15513 language_settings.defaults.completions = Some(CompletionSettingsContent {
15514 words_min_length: Some(0),
15515 lsp: Some(false),
15516 lsp_insert_mode: Some(LspInsertMode::Insert),
15517 ..Default::default()
15518 });
15519 });
15520
15521 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15522
15523 cx.set_state(indoc! {"ˇ
15524 0_usize
15525 let
15526 33
15527 4.5f32
15528 "});
15529 cx.update_editor(|editor, window, cx| {
15530 editor.show_completions(&ShowCompletions, window, cx);
15531 });
15532 cx.executor().run_until_parked();
15533 cx.condition(|editor, _| editor.context_menu_visible())
15534 .await;
15535 cx.update_editor(|editor, window, cx| {
15536 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15537 {
15538 assert_eq!(
15539 completion_menu_entries(menu),
15540 &["let"],
15541 "With no digits in the completion query, no digits should be in the word completions"
15542 );
15543 } else {
15544 panic!("expected completion menu to be open");
15545 }
15546 editor.cancel(&Cancel, window, cx);
15547 });
15548
15549 cx.set_state(indoc! {"3ˇ
15550 0_usize
15551 let
15552 3
15553 33.35f32
15554 "});
15555 cx.update_editor(|editor, window, cx| {
15556 editor.show_completions(&ShowCompletions, window, cx);
15557 });
15558 cx.executor().run_until_parked();
15559 cx.condition(|editor, _| editor.context_menu_visible())
15560 .await;
15561 cx.update_editor(|editor, _, _| {
15562 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15563 {
15564 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
15565 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
15566 } else {
15567 panic!("expected completion menu to be open");
15568 }
15569 });
15570}
15571
15572#[gpui::test]
15573async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
15574 init_test(cx, |language_settings| {
15575 language_settings.defaults.completions = Some(CompletionSettingsContent {
15576 words: Some(WordsCompletionMode::Enabled),
15577 words_min_length: Some(3),
15578 lsp_insert_mode: Some(LspInsertMode::Insert),
15579 ..Default::default()
15580 });
15581 });
15582
15583 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15584 cx.set_state(indoc! {"ˇ
15585 wow
15586 wowen
15587 wowser
15588 "});
15589 cx.simulate_keystroke("w");
15590 cx.executor().run_until_parked();
15591 cx.update_editor(|editor, _, _| {
15592 if editor.context_menu.borrow_mut().is_some() {
15593 panic!(
15594 "expected completion menu to be hidden, as words completion threshold is not met"
15595 );
15596 }
15597 });
15598
15599 cx.update_editor(|editor, window, cx| {
15600 editor.show_word_completions(&ShowWordCompletions, window, cx);
15601 });
15602 cx.executor().run_until_parked();
15603 cx.update_editor(|editor, window, cx| {
15604 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15605 {
15606 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");
15607 } else {
15608 panic!("expected completion menu to be open after the word completions are called with an action");
15609 }
15610
15611 editor.cancel(&Cancel, window, cx);
15612 });
15613 cx.update_editor(|editor, _, _| {
15614 if editor.context_menu.borrow_mut().is_some() {
15615 panic!("expected completion menu to be hidden after canceling");
15616 }
15617 });
15618
15619 cx.simulate_keystroke("o");
15620 cx.executor().run_until_parked();
15621 cx.update_editor(|editor, _, _| {
15622 if editor.context_menu.borrow_mut().is_some() {
15623 panic!(
15624 "expected completion menu to be hidden, as words completion threshold is not met still"
15625 );
15626 }
15627 });
15628
15629 cx.simulate_keystroke("w");
15630 cx.executor().run_until_parked();
15631 cx.update_editor(|editor, _, _| {
15632 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15633 {
15634 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
15635 } else {
15636 panic!("expected completion menu to be open after the word completions threshold is met");
15637 }
15638 });
15639}
15640
15641#[gpui::test]
15642async fn test_word_completions_disabled(cx: &mut TestAppContext) {
15643 init_test(cx, |language_settings| {
15644 language_settings.defaults.completions = Some(CompletionSettingsContent {
15645 words: Some(WordsCompletionMode::Enabled),
15646 words_min_length: Some(0),
15647 lsp_insert_mode: Some(LspInsertMode::Insert),
15648 ..Default::default()
15649 });
15650 });
15651
15652 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15653 cx.update_editor(|editor, _, _| {
15654 editor.disable_word_completions();
15655 });
15656 cx.set_state(indoc! {"ˇ
15657 wow
15658 wowen
15659 wowser
15660 "});
15661 cx.simulate_keystroke("w");
15662 cx.executor().run_until_parked();
15663 cx.update_editor(|editor, _, _| {
15664 if editor.context_menu.borrow_mut().is_some() {
15665 panic!(
15666 "expected completion menu to be hidden, as words completion are disabled for this editor"
15667 );
15668 }
15669 });
15670
15671 cx.update_editor(|editor, window, cx| {
15672 editor.show_word_completions(&ShowWordCompletions, window, cx);
15673 });
15674 cx.executor().run_until_parked();
15675 cx.update_editor(|editor, _, _| {
15676 if editor.context_menu.borrow_mut().is_some() {
15677 panic!(
15678 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
15679 );
15680 }
15681 });
15682}
15683
15684#[gpui::test]
15685async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
15686 init_test(cx, |language_settings| {
15687 language_settings.defaults.completions = Some(CompletionSettingsContent {
15688 words: Some(WordsCompletionMode::Disabled),
15689 words_min_length: Some(0),
15690 lsp_insert_mode: Some(LspInsertMode::Insert),
15691 ..Default::default()
15692 });
15693 });
15694
15695 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15696 cx.update_editor(|editor, _, _| {
15697 editor.set_completion_provider(None);
15698 });
15699 cx.set_state(indoc! {"ˇ
15700 wow
15701 wowen
15702 wowser
15703 "});
15704 cx.simulate_keystroke("w");
15705 cx.executor().run_until_parked();
15706 cx.update_editor(|editor, _, _| {
15707 if editor.context_menu.borrow_mut().is_some() {
15708 panic!("expected completion menu to be hidden, as disabled in settings");
15709 }
15710 });
15711}
15712
15713fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
15714 let position = || lsp::Position {
15715 line: params.text_document_position.position.line,
15716 character: params.text_document_position.position.character,
15717 };
15718 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15719 range: lsp::Range {
15720 start: position(),
15721 end: position(),
15722 },
15723 new_text: text.to_string(),
15724 }))
15725}
15726
15727#[gpui::test]
15728async fn test_multiline_completion(cx: &mut TestAppContext) {
15729 init_test(cx, |_| {});
15730
15731 let fs = FakeFs::new(cx.executor());
15732 fs.insert_tree(
15733 path!("/a"),
15734 json!({
15735 "main.ts": "a",
15736 }),
15737 )
15738 .await;
15739
15740 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15741 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15742 let typescript_language = Arc::new(Language::new(
15743 LanguageConfig {
15744 name: "TypeScript".into(),
15745 matcher: LanguageMatcher {
15746 path_suffixes: vec!["ts".to_string()],
15747 ..LanguageMatcher::default()
15748 },
15749 line_comments: vec!["// ".into()],
15750 ..LanguageConfig::default()
15751 },
15752 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15753 ));
15754 language_registry.add(typescript_language.clone());
15755 let mut fake_servers = language_registry.register_fake_lsp(
15756 "TypeScript",
15757 FakeLspAdapter {
15758 capabilities: lsp::ServerCapabilities {
15759 completion_provider: Some(lsp::CompletionOptions {
15760 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15761 ..lsp::CompletionOptions::default()
15762 }),
15763 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15764 ..lsp::ServerCapabilities::default()
15765 },
15766 // Emulate vtsls label generation
15767 label_for_completion: Some(Box::new(|item, _| {
15768 let text = if let Some(description) = item
15769 .label_details
15770 .as_ref()
15771 .and_then(|label_details| label_details.description.as_ref())
15772 {
15773 format!("{} {}", item.label, description)
15774 } else if let Some(detail) = &item.detail {
15775 format!("{} {}", item.label, detail)
15776 } else {
15777 item.label.clone()
15778 };
15779 Some(language::CodeLabel::plain(text, None))
15780 })),
15781 ..FakeLspAdapter::default()
15782 },
15783 );
15784 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15785 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15786 let worktree_id = workspace
15787 .update(cx, |workspace, _window, cx| {
15788 workspace.project().update(cx, |project, cx| {
15789 project.worktrees(cx).next().unwrap().read(cx).id()
15790 })
15791 })
15792 .unwrap();
15793 let _buffer = project
15794 .update(cx, |project, cx| {
15795 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15796 })
15797 .await
15798 .unwrap();
15799 let editor = workspace
15800 .update(cx, |workspace, window, cx| {
15801 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15802 })
15803 .unwrap()
15804 .await
15805 .unwrap()
15806 .downcast::<Editor>()
15807 .unwrap();
15808 let fake_server = fake_servers.next().await.unwrap();
15809
15810 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15811 let multiline_label_2 = "a\nb\nc\n";
15812 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15813 let multiline_description = "d\ne\nf\n";
15814 let multiline_detail_2 = "g\nh\ni\n";
15815
15816 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15817 move |params, _| async move {
15818 Ok(Some(lsp::CompletionResponse::Array(vec![
15819 lsp::CompletionItem {
15820 label: multiline_label.to_string(),
15821 text_edit: gen_text_edit(¶ms, "new_text_1"),
15822 ..lsp::CompletionItem::default()
15823 },
15824 lsp::CompletionItem {
15825 label: "single line label 1".to_string(),
15826 detail: Some(multiline_detail.to_string()),
15827 text_edit: gen_text_edit(¶ms, "new_text_2"),
15828 ..lsp::CompletionItem::default()
15829 },
15830 lsp::CompletionItem {
15831 label: "single line label 2".to_string(),
15832 label_details: Some(lsp::CompletionItemLabelDetails {
15833 description: Some(multiline_description.to_string()),
15834 detail: None,
15835 }),
15836 text_edit: gen_text_edit(¶ms, "new_text_2"),
15837 ..lsp::CompletionItem::default()
15838 },
15839 lsp::CompletionItem {
15840 label: multiline_label_2.to_string(),
15841 detail: Some(multiline_detail_2.to_string()),
15842 text_edit: gen_text_edit(¶ms, "new_text_3"),
15843 ..lsp::CompletionItem::default()
15844 },
15845 lsp::CompletionItem {
15846 label: "Label with many spaces and \t but without newlines".to_string(),
15847 detail: Some(
15848 "Details with many spaces and \t but without newlines".to_string(),
15849 ),
15850 text_edit: gen_text_edit(¶ms, "new_text_4"),
15851 ..lsp::CompletionItem::default()
15852 },
15853 ])))
15854 },
15855 );
15856
15857 editor.update_in(cx, |editor, window, cx| {
15858 cx.focus_self(window);
15859 editor.move_to_end(&MoveToEnd, window, cx);
15860 editor.handle_input(".", window, cx);
15861 });
15862 cx.run_until_parked();
15863 completion_handle.next().await.unwrap();
15864
15865 editor.update(cx, |editor, _| {
15866 assert!(editor.context_menu_visible());
15867 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15868 {
15869 let completion_labels = menu
15870 .completions
15871 .borrow()
15872 .iter()
15873 .map(|c| c.label.text.clone())
15874 .collect::<Vec<_>>();
15875 assert_eq!(
15876 completion_labels,
15877 &[
15878 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15879 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15880 "single line label 2 d e f ",
15881 "a b c g h i ",
15882 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15883 ],
15884 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15885 );
15886
15887 for completion in menu
15888 .completions
15889 .borrow()
15890 .iter() {
15891 assert_eq!(
15892 completion.label.filter_range,
15893 0..completion.label.text.len(),
15894 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15895 );
15896 }
15897 } else {
15898 panic!("expected completion menu to be open");
15899 }
15900 });
15901}
15902
15903#[gpui::test]
15904async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15905 init_test(cx, |_| {});
15906 let mut cx = EditorLspTestContext::new_rust(
15907 lsp::ServerCapabilities {
15908 completion_provider: Some(lsp::CompletionOptions {
15909 trigger_characters: Some(vec![".".to_string()]),
15910 ..Default::default()
15911 }),
15912 ..Default::default()
15913 },
15914 cx,
15915 )
15916 .await;
15917 cx.lsp
15918 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15919 Ok(Some(lsp::CompletionResponse::Array(vec![
15920 lsp::CompletionItem {
15921 label: "first".into(),
15922 ..Default::default()
15923 },
15924 lsp::CompletionItem {
15925 label: "last".into(),
15926 ..Default::default()
15927 },
15928 ])))
15929 });
15930 cx.set_state("variableˇ");
15931 cx.simulate_keystroke(".");
15932 cx.executor().run_until_parked();
15933
15934 cx.update_editor(|editor, _, _| {
15935 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15936 {
15937 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15938 } else {
15939 panic!("expected completion menu to be open");
15940 }
15941 });
15942
15943 cx.update_editor(|editor, window, cx| {
15944 editor.move_page_down(&MovePageDown::default(), window, cx);
15945 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15946 {
15947 assert!(
15948 menu.selected_item == 1,
15949 "expected PageDown to select the last item from the context menu"
15950 );
15951 } else {
15952 panic!("expected completion menu to stay open after PageDown");
15953 }
15954 });
15955
15956 cx.update_editor(|editor, window, cx| {
15957 editor.move_page_up(&MovePageUp::default(), window, cx);
15958 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15959 {
15960 assert!(
15961 menu.selected_item == 0,
15962 "expected PageUp to select the first item from the context menu"
15963 );
15964 } else {
15965 panic!("expected completion menu to stay open after PageUp");
15966 }
15967 });
15968}
15969
15970#[gpui::test]
15971async fn test_as_is_completions(cx: &mut TestAppContext) {
15972 init_test(cx, |_| {});
15973 let mut cx = EditorLspTestContext::new_rust(
15974 lsp::ServerCapabilities {
15975 completion_provider: Some(lsp::CompletionOptions {
15976 ..Default::default()
15977 }),
15978 ..Default::default()
15979 },
15980 cx,
15981 )
15982 .await;
15983 cx.lsp
15984 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15985 Ok(Some(lsp::CompletionResponse::Array(vec![
15986 lsp::CompletionItem {
15987 label: "unsafe".into(),
15988 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15989 range: lsp::Range {
15990 start: lsp::Position {
15991 line: 1,
15992 character: 2,
15993 },
15994 end: lsp::Position {
15995 line: 1,
15996 character: 3,
15997 },
15998 },
15999 new_text: "unsafe".to_string(),
16000 })),
16001 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
16002 ..Default::default()
16003 },
16004 ])))
16005 });
16006 cx.set_state("fn a() {}\n nˇ");
16007 cx.executor().run_until_parked();
16008 cx.update_editor(|editor, window, cx| {
16009 editor.trigger_completion_on_input("n", true, window, cx)
16010 });
16011 cx.executor().run_until_parked();
16012
16013 cx.update_editor(|editor, window, cx| {
16014 editor.confirm_completion(&Default::default(), window, cx)
16015 });
16016 cx.executor().run_until_parked();
16017 cx.assert_editor_state("fn a() {}\n unsafeˇ");
16018}
16019
16020#[gpui::test]
16021async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
16022 init_test(cx, |_| {});
16023 let language =
16024 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
16025 let mut cx = EditorLspTestContext::new(
16026 language,
16027 lsp::ServerCapabilities {
16028 completion_provider: Some(lsp::CompletionOptions {
16029 ..lsp::CompletionOptions::default()
16030 }),
16031 ..lsp::ServerCapabilities::default()
16032 },
16033 cx,
16034 )
16035 .await;
16036
16037 cx.set_state(
16038 "#ifndef BAR_H
16039#define BAR_H
16040
16041#include <stdbool.h>
16042
16043int fn_branch(bool do_branch1, bool do_branch2);
16044
16045#endif // BAR_H
16046ˇ",
16047 );
16048 cx.executor().run_until_parked();
16049 cx.update_editor(|editor, window, cx| {
16050 editor.handle_input("#", window, cx);
16051 });
16052 cx.executor().run_until_parked();
16053 cx.update_editor(|editor, window, cx| {
16054 editor.handle_input("i", window, cx);
16055 });
16056 cx.executor().run_until_parked();
16057 cx.update_editor(|editor, window, cx| {
16058 editor.handle_input("n", window, cx);
16059 });
16060 cx.executor().run_until_parked();
16061 cx.assert_editor_state(
16062 "#ifndef BAR_H
16063#define BAR_H
16064
16065#include <stdbool.h>
16066
16067int fn_branch(bool do_branch1, bool do_branch2);
16068
16069#endif // BAR_H
16070#inˇ",
16071 );
16072
16073 cx.lsp
16074 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16075 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16076 is_incomplete: false,
16077 item_defaults: None,
16078 items: vec![lsp::CompletionItem {
16079 kind: Some(lsp::CompletionItemKind::SNIPPET),
16080 label_details: Some(lsp::CompletionItemLabelDetails {
16081 detail: Some("header".to_string()),
16082 description: None,
16083 }),
16084 label: " include".to_string(),
16085 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16086 range: lsp::Range {
16087 start: lsp::Position {
16088 line: 8,
16089 character: 1,
16090 },
16091 end: lsp::Position {
16092 line: 8,
16093 character: 1,
16094 },
16095 },
16096 new_text: "include \"$0\"".to_string(),
16097 })),
16098 sort_text: Some("40b67681include".to_string()),
16099 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16100 filter_text: Some("include".to_string()),
16101 insert_text: Some("include \"$0\"".to_string()),
16102 ..lsp::CompletionItem::default()
16103 }],
16104 })))
16105 });
16106 cx.update_editor(|editor, window, cx| {
16107 editor.show_completions(&ShowCompletions, window, cx);
16108 });
16109 cx.executor().run_until_parked();
16110 cx.update_editor(|editor, window, cx| {
16111 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16112 });
16113 cx.executor().run_until_parked();
16114 cx.assert_editor_state(
16115 "#ifndef BAR_H
16116#define BAR_H
16117
16118#include <stdbool.h>
16119
16120int fn_branch(bool do_branch1, bool do_branch2);
16121
16122#endif // BAR_H
16123#include \"ˇ\"",
16124 );
16125
16126 cx.lsp
16127 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16128 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16129 is_incomplete: true,
16130 item_defaults: None,
16131 items: vec![lsp::CompletionItem {
16132 kind: Some(lsp::CompletionItemKind::FILE),
16133 label: "AGL/".to_string(),
16134 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16135 range: lsp::Range {
16136 start: lsp::Position {
16137 line: 8,
16138 character: 10,
16139 },
16140 end: lsp::Position {
16141 line: 8,
16142 character: 11,
16143 },
16144 },
16145 new_text: "AGL/".to_string(),
16146 })),
16147 sort_text: Some("40b67681AGL/".to_string()),
16148 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16149 filter_text: Some("AGL/".to_string()),
16150 insert_text: Some("AGL/".to_string()),
16151 ..lsp::CompletionItem::default()
16152 }],
16153 })))
16154 });
16155 cx.update_editor(|editor, window, cx| {
16156 editor.show_completions(&ShowCompletions, window, cx);
16157 });
16158 cx.executor().run_until_parked();
16159 cx.update_editor(|editor, window, cx| {
16160 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16161 });
16162 cx.executor().run_until_parked();
16163 cx.assert_editor_state(
16164 r##"#ifndef BAR_H
16165#define BAR_H
16166
16167#include <stdbool.h>
16168
16169int fn_branch(bool do_branch1, bool do_branch2);
16170
16171#endif // BAR_H
16172#include "AGL/ˇ"##,
16173 );
16174
16175 cx.update_editor(|editor, window, cx| {
16176 editor.handle_input("\"", window, cx);
16177 });
16178 cx.executor().run_until_parked();
16179 cx.assert_editor_state(
16180 r##"#ifndef BAR_H
16181#define BAR_H
16182
16183#include <stdbool.h>
16184
16185int fn_branch(bool do_branch1, bool do_branch2);
16186
16187#endif // BAR_H
16188#include "AGL/"ˇ"##,
16189 );
16190}
16191
16192#[gpui::test]
16193async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
16194 init_test(cx, |_| {});
16195
16196 let mut cx = EditorLspTestContext::new_rust(
16197 lsp::ServerCapabilities {
16198 completion_provider: Some(lsp::CompletionOptions {
16199 trigger_characters: Some(vec![".".to_string()]),
16200 resolve_provider: Some(true),
16201 ..Default::default()
16202 }),
16203 ..Default::default()
16204 },
16205 cx,
16206 )
16207 .await;
16208
16209 cx.set_state("fn main() { let a = 2ˇ; }");
16210 cx.simulate_keystroke(".");
16211 let completion_item = lsp::CompletionItem {
16212 label: "Some".into(),
16213 kind: Some(lsp::CompletionItemKind::SNIPPET),
16214 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
16215 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
16216 kind: lsp::MarkupKind::Markdown,
16217 value: "```rust\nSome(2)\n```".to_string(),
16218 })),
16219 deprecated: Some(false),
16220 sort_text: Some("Some".to_string()),
16221 filter_text: Some("Some".to_string()),
16222 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16223 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16224 range: lsp::Range {
16225 start: lsp::Position {
16226 line: 0,
16227 character: 22,
16228 },
16229 end: lsp::Position {
16230 line: 0,
16231 character: 22,
16232 },
16233 },
16234 new_text: "Some(2)".to_string(),
16235 })),
16236 additional_text_edits: Some(vec![lsp::TextEdit {
16237 range: lsp::Range {
16238 start: lsp::Position {
16239 line: 0,
16240 character: 20,
16241 },
16242 end: lsp::Position {
16243 line: 0,
16244 character: 22,
16245 },
16246 },
16247 new_text: "".to_string(),
16248 }]),
16249 ..Default::default()
16250 };
16251
16252 let closure_completion_item = completion_item.clone();
16253 let counter = Arc::new(AtomicUsize::new(0));
16254 let counter_clone = counter.clone();
16255 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16256 let task_completion_item = closure_completion_item.clone();
16257 counter_clone.fetch_add(1, atomic::Ordering::Release);
16258 async move {
16259 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16260 is_incomplete: true,
16261 item_defaults: None,
16262 items: vec![task_completion_item],
16263 })))
16264 }
16265 });
16266
16267 cx.condition(|editor, _| editor.context_menu_visible())
16268 .await;
16269 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
16270 assert!(request.next().await.is_some());
16271 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16272
16273 cx.simulate_keystrokes("S o m");
16274 cx.condition(|editor, _| editor.context_menu_visible())
16275 .await;
16276 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
16277 assert!(request.next().await.is_some());
16278 assert!(request.next().await.is_some());
16279 assert!(request.next().await.is_some());
16280 request.close();
16281 assert!(request.next().await.is_none());
16282 assert_eq!(
16283 counter.load(atomic::Ordering::Acquire),
16284 4,
16285 "With the completions menu open, only one LSP request should happen per input"
16286 );
16287}
16288
16289#[gpui::test]
16290async fn test_toggle_comment(cx: &mut TestAppContext) {
16291 init_test(cx, |_| {});
16292 let mut cx = EditorTestContext::new(cx).await;
16293 let language = Arc::new(Language::new(
16294 LanguageConfig {
16295 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16296 ..Default::default()
16297 },
16298 Some(tree_sitter_rust::LANGUAGE.into()),
16299 ));
16300 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16301
16302 // If multiple selections intersect a line, the line is only toggled once.
16303 cx.set_state(indoc! {"
16304 fn a() {
16305 «//b();
16306 ˇ»// «c();
16307 //ˇ» d();
16308 }
16309 "});
16310
16311 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16312
16313 cx.assert_editor_state(indoc! {"
16314 fn a() {
16315 «b();
16316 ˇ»«c();
16317 ˇ» d();
16318 }
16319 "});
16320
16321 // The comment prefix is inserted at the same column for every line in a
16322 // selection.
16323 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16324
16325 cx.assert_editor_state(indoc! {"
16326 fn a() {
16327 // «b();
16328 ˇ»// «c();
16329 ˇ» // d();
16330 }
16331 "});
16332
16333 // If a selection ends at the beginning of a line, that line is not toggled.
16334 cx.set_selections_state(indoc! {"
16335 fn a() {
16336 // b();
16337 «// c();
16338 ˇ» // d();
16339 }
16340 "});
16341
16342 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16343
16344 cx.assert_editor_state(indoc! {"
16345 fn a() {
16346 // b();
16347 «c();
16348 ˇ» // d();
16349 }
16350 "});
16351
16352 // If a selection span a single line and is empty, the line is toggled.
16353 cx.set_state(indoc! {"
16354 fn a() {
16355 a();
16356 b();
16357 ˇ
16358 }
16359 "});
16360
16361 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16362
16363 cx.assert_editor_state(indoc! {"
16364 fn a() {
16365 a();
16366 b();
16367 //•ˇ
16368 }
16369 "});
16370
16371 // If a selection span multiple lines, empty lines are not toggled.
16372 cx.set_state(indoc! {"
16373 fn a() {
16374 «a();
16375
16376 c();ˇ»
16377 }
16378 "});
16379
16380 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16381
16382 cx.assert_editor_state(indoc! {"
16383 fn a() {
16384 // «a();
16385
16386 // c();ˇ»
16387 }
16388 "});
16389
16390 // If a selection includes multiple comment prefixes, all lines are uncommented.
16391 cx.set_state(indoc! {"
16392 fn a() {
16393 «// a();
16394 /// b();
16395 //! c();ˇ»
16396 }
16397 "});
16398
16399 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16400
16401 cx.assert_editor_state(indoc! {"
16402 fn a() {
16403 «a();
16404 b();
16405 c();ˇ»
16406 }
16407 "});
16408}
16409
16410#[gpui::test]
16411async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
16412 init_test(cx, |_| {});
16413 let mut cx = EditorTestContext::new(cx).await;
16414 let language = Arc::new(Language::new(
16415 LanguageConfig {
16416 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16417 ..Default::default()
16418 },
16419 Some(tree_sitter_rust::LANGUAGE.into()),
16420 ));
16421 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16422
16423 let toggle_comments = &ToggleComments {
16424 advance_downwards: false,
16425 ignore_indent: true,
16426 };
16427
16428 // If multiple selections intersect a line, the line is only toggled once.
16429 cx.set_state(indoc! {"
16430 fn a() {
16431 // «b();
16432 // c();
16433 // ˇ» d();
16434 }
16435 "});
16436
16437 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16438
16439 cx.assert_editor_state(indoc! {"
16440 fn a() {
16441 «b();
16442 c();
16443 ˇ» d();
16444 }
16445 "});
16446
16447 // The comment prefix is inserted at the beginning of each line
16448 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16449
16450 cx.assert_editor_state(indoc! {"
16451 fn a() {
16452 // «b();
16453 // c();
16454 // ˇ» d();
16455 }
16456 "});
16457
16458 // If a selection ends at the beginning of a line, that line is not toggled.
16459 cx.set_selections_state(indoc! {"
16460 fn a() {
16461 // b();
16462 // «c();
16463 ˇ»// d();
16464 }
16465 "});
16466
16467 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16468
16469 cx.assert_editor_state(indoc! {"
16470 fn a() {
16471 // b();
16472 «c();
16473 ˇ»// d();
16474 }
16475 "});
16476
16477 // If a selection span a single line and is empty, the line is toggled.
16478 cx.set_state(indoc! {"
16479 fn a() {
16480 a();
16481 b();
16482 ˇ
16483 }
16484 "});
16485
16486 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16487
16488 cx.assert_editor_state(indoc! {"
16489 fn a() {
16490 a();
16491 b();
16492 //ˇ
16493 }
16494 "});
16495
16496 // If a selection span multiple lines, empty lines are not toggled.
16497 cx.set_state(indoc! {"
16498 fn a() {
16499 «a();
16500
16501 c();ˇ»
16502 }
16503 "});
16504
16505 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16506
16507 cx.assert_editor_state(indoc! {"
16508 fn a() {
16509 // «a();
16510
16511 // c();ˇ»
16512 }
16513 "});
16514
16515 // If a selection includes multiple comment prefixes, all lines are uncommented.
16516 cx.set_state(indoc! {"
16517 fn a() {
16518 // «a();
16519 /// b();
16520 //! c();ˇ»
16521 }
16522 "});
16523
16524 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16525
16526 cx.assert_editor_state(indoc! {"
16527 fn a() {
16528 «a();
16529 b();
16530 c();ˇ»
16531 }
16532 "});
16533}
16534
16535#[gpui::test]
16536async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
16537 init_test(cx, |_| {});
16538
16539 let language = Arc::new(Language::new(
16540 LanguageConfig {
16541 line_comments: vec!["// ".into()],
16542 ..Default::default()
16543 },
16544 Some(tree_sitter_rust::LANGUAGE.into()),
16545 ));
16546
16547 let mut cx = EditorTestContext::new(cx).await;
16548
16549 cx.language_registry().add(language.clone());
16550 cx.update_buffer(|buffer, cx| {
16551 buffer.set_language(Some(language), cx);
16552 });
16553
16554 let toggle_comments = &ToggleComments {
16555 advance_downwards: true,
16556 ignore_indent: false,
16557 };
16558
16559 // Single cursor on one line -> advance
16560 // Cursor moves horizontally 3 characters as well on non-blank line
16561 cx.set_state(indoc!(
16562 "fn a() {
16563 ˇdog();
16564 cat();
16565 }"
16566 ));
16567 cx.update_editor(|editor, window, cx| {
16568 editor.toggle_comments(toggle_comments, window, cx);
16569 });
16570 cx.assert_editor_state(indoc!(
16571 "fn a() {
16572 // dog();
16573 catˇ();
16574 }"
16575 ));
16576
16577 // Single selection on one line -> don't advance
16578 cx.set_state(indoc!(
16579 "fn a() {
16580 «dog()ˇ»;
16581 cat();
16582 }"
16583 ));
16584 cx.update_editor(|editor, window, cx| {
16585 editor.toggle_comments(toggle_comments, window, cx);
16586 });
16587 cx.assert_editor_state(indoc!(
16588 "fn a() {
16589 // «dog()ˇ»;
16590 cat();
16591 }"
16592 ));
16593
16594 // Multiple cursors on one line -> advance
16595 cx.set_state(indoc!(
16596 "fn a() {
16597 ˇdˇog();
16598 cat();
16599 }"
16600 ));
16601 cx.update_editor(|editor, window, cx| {
16602 editor.toggle_comments(toggle_comments, window, cx);
16603 });
16604 cx.assert_editor_state(indoc!(
16605 "fn a() {
16606 // dog();
16607 catˇ(ˇ);
16608 }"
16609 ));
16610
16611 // Multiple cursors on one line, with selection -> don't advance
16612 cx.set_state(indoc!(
16613 "fn a() {
16614 ˇdˇog«()ˇ»;
16615 cat();
16616 }"
16617 ));
16618 cx.update_editor(|editor, window, cx| {
16619 editor.toggle_comments(toggle_comments, window, cx);
16620 });
16621 cx.assert_editor_state(indoc!(
16622 "fn a() {
16623 // ˇdˇog«()ˇ»;
16624 cat();
16625 }"
16626 ));
16627
16628 // Single cursor on one line -> advance
16629 // Cursor moves to column 0 on blank line
16630 cx.set_state(indoc!(
16631 "fn a() {
16632 ˇdog();
16633
16634 cat();
16635 }"
16636 ));
16637 cx.update_editor(|editor, window, cx| {
16638 editor.toggle_comments(toggle_comments, window, cx);
16639 });
16640 cx.assert_editor_state(indoc!(
16641 "fn a() {
16642 // dog();
16643 ˇ
16644 cat();
16645 }"
16646 ));
16647
16648 // Single cursor on one line -> advance
16649 // Cursor starts and ends at column 0
16650 cx.set_state(indoc!(
16651 "fn a() {
16652 ˇ dog();
16653 cat();
16654 }"
16655 ));
16656 cx.update_editor(|editor, window, cx| {
16657 editor.toggle_comments(toggle_comments, window, cx);
16658 });
16659 cx.assert_editor_state(indoc!(
16660 "fn a() {
16661 // dog();
16662 ˇ cat();
16663 }"
16664 ));
16665}
16666
16667#[gpui::test]
16668async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16669 init_test(cx, |_| {});
16670
16671 let mut cx = EditorTestContext::new(cx).await;
16672
16673 let html_language = Arc::new(
16674 Language::new(
16675 LanguageConfig {
16676 name: "HTML".into(),
16677 block_comment: Some(BlockCommentConfig {
16678 start: "<!-- ".into(),
16679 prefix: "".into(),
16680 end: " -->".into(),
16681 tab_size: 0,
16682 }),
16683 ..Default::default()
16684 },
16685 Some(tree_sitter_html::LANGUAGE.into()),
16686 )
16687 .with_injection_query(
16688 r#"
16689 (script_element
16690 (raw_text) @injection.content
16691 (#set! injection.language "javascript"))
16692 "#,
16693 )
16694 .unwrap(),
16695 );
16696
16697 let javascript_language = Arc::new(Language::new(
16698 LanguageConfig {
16699 name: "JavaScript".into(),
16700 line_comments: vec!["// ".into()],
16701 ..Default::default()
16702 },
16703 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16704 ));
16705
16706 cx.language_registry().add(html_language.clone());
16707 cx.language_registry().add(javascript_language);
16708 cx.update_buffer(|buffer, cx| {
16709 buffer.set_language(Some(html_language), cx);
16710 });
16711
16712 // Toggle comments for empty selections
16713 cx.set_state(
16714 &r#"
16715 <p>A</p>ˇ
16716 <p>B</p>ˇ
16717 <p>C</p>ˇ
16718 "#
16719 .unindent(),
16720 );
16721 cx.update_editor(|editor, window, cx| {
16722 editor.toggle_comments(&ToggleComments::default(), window, cx)
16723 });
16724 cx.assert_editor_state(
16725 &r#"
16726 <!-- <p>A</p>ˇ -->
16727 <!-- <p>B</p>ˇ -->
16728 <!-- <p>C</p>ˇ -->
16729 "#
16730 .unindent(),
16731 );
16732 cx.update_editor(|editor, window, cx| {
16733 editor.toggle_comments(&ToggleComments::default(), window, cx)
16734 });
16735 cx.assert_editor_state(
16736 &r#"
16737 <p>A</p>ˇ
16738 <p>B</p>ˇ
16739 <p>C</p>ˇ
16740 "#
16741 .unindent(),
16742 );
16743
16744 // Toggle comments for mixture of empty and non-empty selections, where
16745 // multiple selections occupy a given line.
16746 cx.set_state(
16747 &r#"
16748 <p>A«</p>
16749 <p>ˇ»B</p>ˇ
16750 <p>C«</p>
16751 <p>ˇ»D</p>ˇ
16752 "#
16753 .unindent(),
16754 );
16755
16756 cx.update_editor(|editor, window, cx| {
16757 editor.toggle_comments(&ToggleComments::default(), window, cx)
16758 });
16759 cx.assert_editor_state(
16760 &r#"
16761 <!-- <p>A«</p>
16762 <p>ˇ»B</p>ˇ -->
16763 <!-- <p>C«</p>
16764 <p>ˇ»D</p>ˇ -->
16765 "#
16766 .unindent(),
16767 );
16768 cx.update_editor(|editor, window, cx| {
16769 editor.toggle_comments(&ToggleComments::default(), window, cx)
16770 });
16771 cx.assert_editor_state(
16772 &r#"
16773 <p>A«</p>
16774 <p>ˇ»B</p>ˇ
16775 <p>C«</p>
16776 <p>ˇ»D</p>ˇ
16777 "#
16778 .unindent(),
16779 );
16780
16781 // Toggle comments when different languages are active for different
16782 // selections.
16783 cx.set_state(
16784 &r#"
16785 ˇ<script>
16786 ˇvar x = new Y();
16787 ˇ</script>
16788 "#
16789 .unindent(),
16790 );
16791 cx.executor().run_until_parked();
16792 cx.update_editor(|editor, window, cx| {
16793 editor.toggle_comments(&ToggleComments::default(), window, cx)
16794 });
16795 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16796 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16797 cx.assert_editor_state(
16798 &r#"
16799 <!-- ˇ<script> -->
16800 // ˇvar x = new Y();
16801 <!-- ˇ</script> -->
16802 "#
16803 .unindent(),
16804 );
16805}
16806
16807#[gpui::test]
16808fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16809 init_test(cx, |_| {});
16810
16811 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16812 let multibuffer = cx.new(|cx| {
16813 let mut multibuffer = MultiBuffer::new(ReadWrite);
16814 multibuffer.push_excerpts(
16815 buffer.clone(),
16816 [
16817 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16818 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16819 ],
16820 cx,
16821 );
16822 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16823 multibuffer
16824 });
16825
16826 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16827 editor.update_in(cx, |editor, window, cx| {
16828 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16829 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16830 s.select_ranges([
16831 Point::new(0, 0)..Point::new(0, 0),
16832 Point::new(1, 0)..Point::new(1, 0),
16833 ])
16834 });
16835
16836 editor.handle_input("X", window, cx);
16837 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16838 assert_eq!(
16839 editor.selections.ranges(&editor.display_snapshot(cx)),
16840 [
16841 Point::new(0, 1)..Point::new(0, 1),
16842 Point::new(1, 1)..Point::new(1, 1),
16843 ]
16844 );
16845
16846 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16847 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16848 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16849 });
16850 editor.backspace(&Default::default(), window, cx);
16851 assert_eq!(editor.text(cx), "Xa\nbbb");
16852 assert_eq!(
16853 editor.selections.ranges(&editor.display_snapshot(cx)),
16854 [Point::new(1, 0)..Point::new(1, 0)]
16855 );
16856
16857 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16858 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16859 });
16860 editor.backspace(&Default::default(), window, cx);
16861 assert_eq!(editor.text(cx), "X\nbb");
16862 assert_eq!(
16863 editor.selections.ranges(&editor.display_snapshot(cx)),
16864 [Point::new(0, 1)..Point::new(0, 1)]
16865 );
16866 });
16867}
16868
16869#[gpui::test]
16870fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16871 init_test(cx, |_| {});
16872
16873 let markers = vec![('[', ']').into(), ('(', ')').into()];
16874 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16875 indoc! {"
16876 [aaaa
16877 (bbbb]
16878 cccc)",
16879 },
16880 markers.clone(),
16881 );
16882 let excerpt_ranges = markers.into_iter().map(|marker| {
16883 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16884 ExcerptRange::new(context)
16885 });
16886 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16887 let multibuffer = cx.new(|cx| {
16888 let mut multibuffer = MultiBuffer::new(ReadWrite);
16889 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16890 multibuffer
16891 });
16892
16893 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16894 editor.update_in(cx, |editor, window, cx| {
16895 let (expected_text, selection_ranges) = marked_text_ranges(
16896 indoc! {"
16897 aaaa
16898 bˇbbb
16899 bˇbbˇb
16900 cccc"
16901 },
16902 true,
16903 );
16904 assert_eq!(editor.text(cx), expected_text);
16905 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16906 s.select_ranges(
16907 selection_ranges
16908 .iter()
16909 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
16910 )
16911 });
16912
16913 editor.handle_input("X", window, cx);
16914
16915 let (expected_text, expected_selections) = marked_text_ranges(
16916 indoc! {"
16917 aaaa
16918 bXˇbbXb
16919 bXˇbbXˇb
16920 cccc"
16921 },
16922 false,
16923 );
16924 assert_eq!(editor.text(cx), expected_text);
16925 assert_eq!(
16926 editor.selections.ranges(&editor.display_snapshot(cx)),
16927 expected_selections
16928 .iter()
16929 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
16930 .collect::<Vec<_>>()
16931 );
16932
16933 editor.newline(&Newline, window, cx);
16934 let (expected_text, expected_selections) = marked_text_ranges(
16935 indoc! {"
16936 aaaa
16937 bX
16938 ˇbbX
16939 b
16940 bX
16941 ˇbbX
16942 ˇb
16943 cccc"
16944 },
16945 false,
16946 );
16947 assert_eq!(editor.text(cx), expected_text);
16948 assert_eq!(
16949 editor.selections.ranges(&editor.display_snapshot(cx)),
16950 expected_selections
16951 .iter()
16952 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
16953 .collect::<Vec<_>>()
16954 );
16955 });
16956}
16957
16958#[gpui::test]
16959fn test_refresh_selections(cx: &mut TestAppContext) {
16960 init_test(cx, |_| {});
16961
16962 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16963 let mut excerpt1_id = None;
16964 let multibuffer = cx.new(|cx| {
16965 let mut multibuffer = MultiBuffer::new(ReadWrite);
16966 excerpt1_id = multibuffer
16967 .push_excerpts(
16968 buffer.clone(),
16969 [
16970 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16971 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16972 ],
16973 cx,
16974 )
16975 .into_iter()
16976 .next();
16977 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16978 multibuffer
16979 });
16980
16981 let editor = cx.add_window(|window, cx| {
16982 let mut editor = build_editor(multibuffer.clone(), window, cx);
16983 let snapshot = editor.snapshot(window, cx);
16984 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16985 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16986 });
16987 editor.begin_selection(
16988 Point::new(2, 1).to_display_point(&snapshot),
16989 true,
16990 1,
16991 window,
16992 cx,
16993 );
16994 assert_eq!(
16995 editor.selections.ranges(&editor.display_snapshot(cx)),
16996 [
16997 Point::new(1, 3)..Point::new(1, 3),
16998 Point::new(2, 1)..Point::new(2, 1),
16999 ]
17000 );
17001 editor
17002 });
17003
17004 // Refreshing selections is a no-op when excerpts haven't changed.
17005 _ = editor.update(cx, |editor, window, cx| {
17006 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17007 assert_eq!(
17008 editor.selections.ranges(&editor.display_snapshot(cx)),
17009 [
17010 Point::new(1, 3)..Point::new(1, 3),
17011 Point::new(2, 1)..Point::new(2, 1),
17012 ]
17013 );
17014 });
17015
17016 multibuffer.update(cx, |multibuffer, cx| {
17017 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
17018 });
17019 _ = editor.update(cx, |editor, window, cx| {
17020 // Removing an excerpt causes the first selection to become degenerate.
17021 assert_eq!(
17022 editor.selections.ranges(&editor.display_snapshot(cx)),
17023 [
17024 Point::new(0, 0)..Point::new(0, 0),
17025 Point::new(0, 1)..Point::new(0, 1)
17026 ]
17027 );
17028
17029 // Refreshing selections will relocate the first selection to the original buffer
17030 // location.
17031 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17032 assert_eq!(
17033 editor.selections.ranges(&editor.display_snapshot(cx)),
17034 [
17035 Point::new(0, 1)..Point::new(0, 1),
17036 Point::new(0, 3)..Point::new(0, 3)
17037 ]
17038 );
17039 assert!(editor.selections.pending_anchor().is_some());
17040 });
17041}
17042
17043#[gpui::test]
17044fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
17045 init_test(cx, |_| {});
17046
17047 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
17048 let mut excerpt1_id = None;
17049 let multibuffer = cx.new(|cx| {
17050 let mut multibuffer = MultiBuffer::new(ReadWrite);
17051 excerpt1_id = multibuffer
17052 .push_excerpts(
17053 buffer.clone(),
17054 [
17055 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
17056 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
17057 ],
17058 cx,
17059 )
17060 .into_iter()
17061 .next();
17062 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
17063 multibuffer
17064 });
17065
17066 let editor = cx.add_window(|window, cx| {
17067 let mut editor = build_editor(multibuffer.clone(), window, cx);
17068 let snapshot = editor.snapshot(window, cx);
17069 editor.begin_selection(
17070 Point::new(1, 3).to_display_point(&snapshot),
17071 false,
17072 1,
17073 window,
17074 cx,
17075 );
17076 assert_eq!(
17077 editor.selections.ranges(&editor.display_snapshot(cx)),
17078 [Point::new(1, 3)..Point::new(1, 3)]
17079 );
17080 editor
17081 });
17082
17083 multibuffer.update(cx, |multibuffer, cx| {
17084 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
17085 });
17086 _ = editor.update(cx, |editor, window, cx| {
17087 assert_eq!(
17088 editor.selections.ranges(&editor.display_snapshot(cx)),
17089 [Point::new(0, 0)..Point::new(0, 0)]
17090 );
17091
17092 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
17093 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17094 assert_eq!(
17095 editor.selections.ranges(&editor.display_snapshot(cx)),
17096 [Point::new(0, 3)..Point::new(0, 3)]
17097 );
17098 assert!(editor.selections.pending_anchor().is_some());
17099 });
17100}
17101
17102#[gpui::test]
17103async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
17104 init_test(cx, |_| {});
17105
17106 let language = Arc::new(
17107 Language::new(
17108 LanguageConfig {
17109 brackets: BracketPairConfig {
17110 pairs: vec![
17111 BracketPair {
17112 start: "{".to_string(),
17113 end: "}".to_string(),
17114 close: true,
17115 surround: true,
17116 newline: true,
17117 },
17118 BracketPair {
17119 start: "/* ".to_string(),
17120 end: " */".to_string(),
17121 close: true,
17122 surround: true,
17123 newline: true,
17124 },
17125 ],
17126 ..Default::default()
17127 },
17128 ..Default::default()
17129 },
17130 Some(tree_sitter_rust::LANGUAGE.into()),
17131 )
17132 .with_indents_query("")
17133 .unwrap(),
17134 );
17135
17136 let text = concat!(
17137 "{ }\n", //
17138 " x\n", //
17139 " /* */\n", //
17140 "x\n", //
17141 "{{} }\n", //
17142 );
17143
17144 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17145 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17146 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
17147 editor
17148 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
17149 .await;
17150
17151 editor.update_in(cx, |editor, window, cx| {
17152 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17153 s.select_display_ranges([
17154 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
17155 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
17156 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
17157 ])
17158 });
17159 editor.newline(&Newline, window, cx);
17160
17161 assert_eq!(
17162 editor.buffer().read(cx).read(cx).text(),
17163 concat!(
17164 "{ \n", // Suppress rustfmt
17165 "\n", //
17166 "}\n", //
17167 " x\n", //
17168 " /* \n", //
17169 " \n", //
17170 " */\n", //
17171 "x\n", //
17172 "{{} \n", //
17173 "}\n", //
17174 )
17175 );
17176 });
17177}
17178
17179#[gpui::test]
17180fn test_highlighted_ranges(cx: &mut TestAppContext) {
17181 init_test(cx, |_| {});
17182
17183 let editor = cx.add_window(|window, cx| {
17184 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
17185 build_editor(buffer, window, cx)
17186 });
17187
17188 _ = editor.update(cx, |editor, window, cx| {
17189 struct Type1;
17190 struct Type2;
17191
17192 let buffer = editor.buffer.read(cx).snapshot(cx);
17193
17194 let anchor_range =
17195 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
17196
17197 editor.highlight_background::<Type1>(
17198 &[
17199 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
17200 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
17201 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
17202 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
17203 ],
17204 |_, _| Hsla::red(),
17205 cx,
17206 );
17207 editor.highlight_background::<Type2>(
17208 &[
17209 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
17210 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
17211 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
17212 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
17213 ],
17214 |_, _| Hsla::green(),
17215 cx,
17216 );
17217
17218 let snapshot = editor.snapshot(window, cx);
17219 let highlighted_ranges = editor.sorted_background_highlights_in_range(
17220 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
17221 &snapshot,
17222 cx.theme(),
17223 );
17224 assert_eq!(
17225 highlighted_ranges,
17226 &[
17227 (
17228 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
17229 Hsla::green(),
17230 ),
17231 (
17232 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
17233 Hsla::red(),
17234 ),
17235 (
17236 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
17237 Hsla::green(),
17238 ),
17239 (
17240 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17241 Hsla::red(),
17242 ),
17243 ]
17244 );
17245 assert_eq!(
17246 editor.sorted_background_highlights_in_range(
17247 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
17248 &snapshot,
17249 cx.theme(),
17250 ),
17251 &[(
17252 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17253 Hsla::red(),
17254 )]
17255 );
17256 });
17257}
17258
17259#[gpui::test]
17260async fn test_following(cx: &mut TestAppContext) {
17261 init_test(cx, |_| {});
17262
17263 let fs = FakeFs::new(cx.executor());
17264 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17265
17266 let buffer = project.update(cx, |project, cx| {
17267 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
17268 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
17269 });
17270 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
17271 let follower = cx.update(|cx| {
17272 cx.open_window(
17273 WindowOptions {
17274 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
17275 gpui::Point::new(px(0.), px(0.)),
17276 gpui::Point::new(px(10.), px(80.)),
17277 ))),
17278 ..Default::default()
17279 },
17280 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
17281 )
17282 .unwrap()
17283 });
17284
17285 let is_still_following = Rc::new(RefCell::new(true));
17286 let follower_edit_event_count = Rc::new(RefCell::new(0));
17287 let pending_update = Rc::new(RefCell::new(None));
17288 let leader_entity = leader.root(cx).unwrap();
17289 let follower_entity = follower.root(cx).unwrap();
17290 _ = follower.update(cx, {
17291 let update = pending_update.clone();
17292 let is_still_following = is_still_following.clone();
17293 let follower_edit_event_count = follower_edit_event_count.clone();
17294 |_, window, cx| {
17295 cx.subscribe_in(
17296 &leader_entity,
17297 window,
17298 move |_, leader, event, window, cx| {
17299 leader.read(cx).add_event_to_update_proto(
17300 event,
17301 &mut update.borrow_mut(),
17302 window,
17303 cx,
17304 );
17305 },
17306 )
17307 .detach();
17308
17309 cx.subscribe_in(
17310 &follower_entity,
17311 window,
17312 move |_, _, event: &EditorEvent, _window, _cx| {
17313 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
17314 *is_still_following.borrow_mut() = false;
17315 }
17316
17317 if let EditorEvent::BufferEdited = event {
17318 *follower_edit_event_count.borrow_mut() += 1;
17319 }
17320 },
17321 )
17322 .detach();
17323 }
17324 });
17325
17326 // Update the selections only
17327 _ = leader.update(cx, |leader, window, cx| {
17328 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17329 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17330 });
17331 });
17332 follower
17333 .update(cx, |follower, window, cx| {
17334 follower.apply_update_proto(
17335 &project,
17336 pending_update.borrow_mut().take().unwrap(),
17337 window,
17338 cx,
17339 )
17340 })
17341 .unwrap()
17342 .await
17343 .unwrap();
17344 _ = follower.update(cx, |follower, _, cx| {
17345 assert_eq!(
17346 follower.selections.ranges(&follower.display_snapshot(cx)),
17347 vec![MultiBufferOffset(1)..MultiBufferOffset(1)]
17348 );
17349 });
17350 assert!(*is_still_following.borrow());
17351 assert_eq!(*follower_edit_event_count.borrow(), 0);
17352
17353 // Update the scroll position only
17354 _ = leader.update(cx, |leader, window, cx| {
17355 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17356 });
17357 follower
17358 .update(cx, |follower, window, cx| {
17359 follower.apply_update_proto(
17360 &project,
17361 pending_update.borrow_mut().take().unwrap(),
17362 window,
17363 cx,
17364 )
17365 })
17366 .unwrap()
17367 .await
17368 .unwrap();
17369 assert_eq!(
17370 follower
17371 .update(cx, |follower, _, cx| follower.scroll_position(cx))
17372 .unwrap(),
17373 gpui::Point::new(1.5, 3.5)
17374 );
17375 assert!(*is_still_following.borrow());
17376 assert_eq!(*follower_edit_event_count.borrow(), 0);
17377
17378 // Update the selections and scroll position. The follower's scroll position is updated
17379 // via autoscroll, not via the leader's exact scroll position.
17380 _ = leader.update(cx, |leader, window, cx| {
17381 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17382 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
17383 });
17384 leader.request_autoscroll(Autoscroll::newest(), cx);
17385 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17386 });
17387 follower
17388 .update(cx, |follower, window, cx| {
17389 follower.apply_update_proto(
17390 &project,
17391 pending_update.borrow_mut().take().unwrap(),
17392 window,
17393 cx,
17394 )
17395 })
17396 .unwrap()
17397 .await
17398 .unwrap();
17399 _ = follower.update(cx, |follower, _, cx| {
17400 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
17401 assert_eq!(
17402 follower.selections.ranges(&follower.display_snapshot(cx)),
17403 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
17404 );
17405 });
17406 assert!(*is_still_following.borrow());
17407
17408 // Creating a pending selection that precedes another selection
17409 _ = leader.update(cx, |leader, window, cx| {
17410 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17411 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17412 });
17413 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
17414 });
17415 follower
17416 .update(cx, |follower, window, cx| {
17417 follower.apply_update_proto(
17418 &project,
17419 pending_update.borrow_mut().take().unwrap(),
17420 window,
17421 cx,
17422 )
17423 })
17424 .unwrap()
17425 .await
17426 .unwrap();
17427 _ = follower.update(cx, |follower, _, cx| {
17428 assert_eq!(
17429 follower.selections.ranges(&follower.display_snapshot(cx)),
17430 vec![
17431 MultiBufferOffset(0)..MultiBufferOffset(0),
17432 MultiBufferOffset(1)..MultiBufferOffset(1)
17433 ]
17434 );
17435 });
17436 assert!(*is_still_following.borrow());
17437
17438 // Extend the pending selection so that it surrounds another selection
17439 _ = leader.update(cx, |leader, window, cx| {
17440 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
17441 });
17442 follower
17443 .update(cx, |follower, window, cx| {
17444 follower.apply_update_proto(
17445 &project,
17446 pending_update.borrow_mut().take().unwrap(),
17447 window,
17448 cx,
17449 )
17450 })
17451 .unwrap()
17452 .await
17453 .unwrap();
17454 _ = follower.update(cx, |follower, _, cx| {
17455 assert_eq!(
17456 follower.selections.ranges(&follower.display_snapshot(cx)),
17457 vec![MultiBufferOffset(0)..MultiBufferOffset(2)]
17458 );
17459 });
17460
17461 // Scrolling locally breaks the follow
17462 _ = follower.update(cx, |follower, window, cx| {
17463 let top_anchor = follower
17464 .buffer()
17465 .read(cx)
17466 .read(cx)
17467 .anchor_after(MultiBufferOffset(0));
17468 follower.set_scroll_anchor(
17469 ScrollAnchor {
17470 anchor: top_anchor,
17471 offset: gpui::Point::new(0.0, 0.5),
17472 },
17473 window,
17474 cx,
17475 );
17476 });
17477 assert!(!(*is_still_following.borrow()));
17478}
17479
17480#[gpui::test]
17481async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
17482 init_test(cx, |_| {});
17483
17484 let fs = FakeFs::new(cx.executor());
17485 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17486 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17487 let pane = workspace
17488 .update(cx, |workspace, _, _| workspace.active_pane().clone())
17489 .unwrap();
17490
17491 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17492
17493 let leader = pane.update_in(cx, |_, window, cx| {
17494 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
17495 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
17496 });
17497
17498 // Start following the editor when it has no excerpts.
17499 let mut state_message =
17500 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17501 let workspace_entity = workspace.root(cx).unwrap();
17502 let follower_1 = cx
17503 .update_window(*workspace.deref(), |_, window, cx| {
17504 Editor::from_state_proto(
17505 workspace_entity,
17506 ViewId {
17507 creator: CollaboratorId::PeerId(PeerId::default()),
17508 id: 0,
17509 },
17510 &mut state_message,
17511 window,
17512 cx,
17513 )
17514 })
17515 .unwrap()
17516 .unwrap()
17517 .await
17518 .unwrap();
17519
17520 let update_message = Rc::new(RefCell::new(None));
17521 follower_1.update_in(cx, {
17522 let update = update_message.clone();
17523 |_, window, cx| {
17524 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
17525 leader.read(cx).add_event_to_update_proto(
17526 event,
17527 &mut update.borrow_mut(),
17528 window,
17529 cx,
17530 );
17531 })
17532 .detach();
17533 }
17534 });
17535
17536 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
17537 (
17538 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
17539 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
17540 )
17541 });
17542
17543 // Insert some excerpts.
17544 leader.update(cx, |leader, cx| {
17545 leader.buffer.update(cx, |multibuffer, cx| {
17546 multibuffer.set_excerpts_for_path(
17547 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
17548 buffer_1.clone(),
17549 vec![
17550 Point::row_range(0..3),
17551 Point::row_range(1..6),
17552 Point::row_range(12..15),
17553 ],
17554 0,
17555 cx,
17556 );
17557 multibuffer.set_excerpts_for_path(
17558 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
17559 buffer_2.clone(),
17560 vec![Point::row_range(0..6), Point::row_range(8..12)],
17561 0,
17562 cx,
17563 );
17564 });
17565 });
17566
17567 // Apply the update of adding the excerpts.
17568 follower_1
17569 .update_in(cx, |follower, window, cx| {
17570 follower.apply_update_proto(
17571 &project,
17572 update_message.borrow().clone().unwrap(),
17573 window,
17574 cx,
17575 )
17576 })
17577 .await
17578 .unwrap();
17579 assert_eq!(
17580 follower_1.update(cx, |editor, cx| editor.text(cx)),
17581 leader.update(cx, |editor, cx| editor.text(cx))
17582 );
17583 update_message.borrow_mut().take();
17584
17585 // Start following separately after it already has excerpts.
17586 let mut state_message =
17587 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17588 let workspace_entity = workspace.root(cx).unwrap();
17589 let follower_2 = cx
17590 .update_window(*workspace.deref(), |_, window, cx| {
17591 Editor::from_state_proto(
17592 workspace_entity,
17593 ViewId {
17594 creator: CollaboratorId::PeerId(PeerId::default()),
17595 id: 0,
17596 },
17597 &mut state_message,
17598 window,
17599 cx,
17600 )
17601 })
17602 .unwrap()
17603 .unwrap()
17604 .await
17605 .unwrap();
17606 assert_eq!(
17607 follower_2.update(cx, |editor, cx| editor.text(cx)),
17608 leader.update(cx, |editor, cx| editor.text(cx))
17609 );
17610
17611 // Remove some excerpts.
17612 leader.update(cx, |leader, cx| {
17613 leader.buffer.update(cx, |multibuffer, cx| {
17614 let excerpt_ids = multibuffer.excerpt_ids();
17615 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
17616 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
17617 });
17618 });
17619
17620 // Apply the update of removing the excerpts.
17621 follower_1
17622 .update_in(cx, |follower, window, cx| {
17623 follower.apply_update_proto(
17624 &project,
17625 update_message.borrow().clone().unwrap(),
17626 window,
17627 cx,
17628 )
17629 })
17630 .await
17631 .unwrap();
17632 follower_2
17633 .update_in(cx, |follower, window, cx| {
17634 follower.apply_update_proto(
17635 &project,
17636 update_message.borrow().clone().unwrap(),
17637 window,
17638 cx,
17639 )
17640 })
17641 .await
17642 .unwrap();
17643 update_message.borrow_mut().take();
17644 assert_eq!(
17645 follower_1.update(cx, |editor, cx| editor.text(cx)),
17646 leader.update(cx, |editor, cx| editor.text(cx))
17647 );
17648}
17649
17650#[gpui::test]
17651async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17652 init_test(cx, |_| {});
17653
17654 let mut cx = EditorTestContext::new(cx).await;
17655 let lsp_store =
17656 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
17657
17658 cx.set_state(indoc! {"
17659 ˇfn func(abc def: i32) -> u32 {
17660 }
17661 "});
17662
17663 cx.update(|_, cx| {
17664 lsp_store.update(cx, |lsp_store, cx| {
17665 lsp_store
17666 .update_diagnostics(
17667 LanguageServerId(0),
17668 lsp::PublishDiagnosticsParams {
17669 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
17670 version: None,
17671 diagnostics: vec![
17672 lsp::Diagnostic {
17673 range: lsp::Range::new(
17674 lsp::Position::new(0, 11),
17675 lsp::Position::new(0, 12),
17676 ),
17677 severity: Some(lsp::DiagnosticSeverity::ERROR),
17678 ..Default::default()
17679 },
17680 lsp::Diagnostic {
17681 range: lsp::Range::new(
17682 lsp::Position::new(0, 12),
17683 lsp::Position::new(0, 15),
17684 ),
17685 severity: Some(lsp::DiagnosticSeverity::ERROR),
17686 ..Default::default()
17687 },
17688 lsp::Diagnostic {
17689 range: lsp::Range::new(
17690 lsp::Position::new(0, 25),
17691 lsp::Position::new(0, 28),
17692 ),
17693 severity: Some(lsp::DiagnosticSeverity::ERROR),
17694 ..Default::default()
17695 },
17696 ],
17697 },
17698 None,
17699 DiagnosticSourceKind::Pushed,
17700 &[],
17701 cx,
17702 )
17703 .unwrap()
17704 });
17705 });
17706
17707 executor.run_until_parked();
17708
17709 cx.update_editor(|editor, window, cx| {
17710 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17711 });
17712
17713 cx.assert_editor_state(indoc! {"
17714 fn func(abc def: i32) -> ˇu32 {
17715 }
17716 "});
17717
17718 cx.update_editor(|editor, window, cx| {
17719 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17720 });
17721
17722 cx.assert_editor_state(indoc! {"
17723 fn func(abc ˇdef: i32) -> u32 {
17724 }
17725 "});
17726
17727 cx.update_editor(|editor, window, cx| {
17728 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17729 });
17730
17731 cx.assert_editor_state(indoc! {"
17732 fn func(abcˇ def: i32) -> u32 {
17733 }
17734 "});
17735
17736 cx.update_editor(|editor, window, cx| {
17737 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17738 });
17739
17740 cx.assert_editor_state(indoc! {"
17741 fn func(abc def: i32) -> ˇu32 {
17742 }
17743 "});
17744}
17745
17746#[gpui::test]
17747async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17748 init_test(cx, |_| {});
17749
17750 let mut cx = EditorTestContext::new(cx).await;
17751
17752 let diff_base = r#"
17753 use some::mod;
17754
17755 const A: u32 = 42;
17756
17757 fn main() {
17758 println!("hello");
17759
17760 println!("world");
17761 }
17762 "#
17763 .unindent();
17764
17765 // Edits are modified, removed, modified, added
17766 cx.set_state(
17767 &r#"
17768 use some::modified;
17769
17770 ˇ
17771 fn main() {
17772 println!("hello there");
17773
17774 println!("around the");
17775 println!("world");
17776 }
17777 "#
17778 .unindent(),
17779 );
17780
17781 cx.set_head_text(&diff_base);
17782 executor.run_until_parked();
17783
17784 cx.update_editor(|editor, window, cx| {
17785 //Wrap around the bottom of the buffer
17786 for _ in 0..3 {
17787 editor.go_to_next_hunk(&GoToHunk, window, cx);
17788 }
17789 });
17790
17791 cx.assert_editor_state(
17792 &r#"
17793 ˇuse some::modified;
17794
17795
17796 fn main() {
17797 println!("hello there");
17798
17799 println!("around the");
17800 println!("world");
17801 }
17802 "#
17803 .unindent(),
17804 );
17805
17806 cx.update_editor(|editor, window, cx| {
17807 //Wrap around the top of the buffer
17808 for _ in 0..2 {
17809 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17810 }
17811 });
17812
17813 cx.assert_editor_state(
17814 &r#"
17815 use some::modified;
17816
17817
17818 fn main() {
17819 ˇ println!("hello there");
17820
17821 println!("around the");
17822 println!("world");
17823 }
17824 "#
17825 .unindent(),
17826 );
17827
17828 cx.update_editor(|editor, window, cx| {
17829 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17830 });
17831
17832 cx.assert_editor_state(
17833 &r#"
17834 use some::modified;
17835
17836 ˇ
17837 fn main() {
17838 println!("hello there");
17839
17840 println!("around the");
17841 println!("world");
17842 }
17843 "#
17844 .unindent(),
17845 );
17846
17847 cx.update_editor(|editor, window, cx| {
17848 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17849 });
17850
17851 cx.assert_editor_state(
17852 &r#"
17853 ˇuse some::modified;
17854
17855
17856 fn main() {
17857 println!("hello there");
17858
17859 println!("around the");
17860 println!("world");
17861 }
17862 "#
17863 .unindent(),
17864 );
17865
17866 cx.update_editor(|editor, window, cx| {
17867 for _ in 0..2 {
17868 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17869 }
17870 });
17871
17872 cx.assert_editor_state(
17873 &r#"
17874 use some::modified;
17875
17876
17877 fn main() {
17878 ˇ println!("hello there");
17879
17880 println!("around the");
17881 println!("world");
17882 }
17883 "#
17884 .unindent(),
17885 );
17886
17887 cx.update_editor(|editor, window, cx| {
17888 editor.fold(&Fold, window, cx);
17889 });
17890
17891 cx.update_editor(|editor, window, cx| {
17892 editor.go_to_next_hunk(&GoToHunk, window, cx);
17893 });
17894
17895 cx.assert_editor_state(
17896 &r#"
17897 ˇuse some::modified;
17898
17899
17900 fn main() {
17901 println!("hello there");
17902
17903 println!("around the");
17904 println!("world");
17905 }
17906 "#
17907 .unindent(),
17908 );
17909}
17910
17911#[test]
17912fn test_split_words() {
17913 fn split(text: &str) -> Vec<&str> {
17914 split_words(text).collect()
17915 }
17916
17917 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17918 assert_eq!(split("hello_world"), &["hello_", "world"]);
17919 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17920 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17921 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17922 assert_eq!(split("helloworld"), &["helloworld"]);
17923
17924 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17925}
17926
17927#[test]
17928fn test_split_words_for_snippet_prefix() {
17929 fn split(text: &str) -> Vec<&str> {
17930 snippet_candidate_suffixes(text, |c| c.is_alphanumeric() || c == '_').collect()
17931 }
17932
17933 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
17934 assert_eq!(split("hello_world"), &["hello_world"]);
17935 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
17936 assert_eq!(split("Hello_World"), &["Hello_World"]);
17937 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
17938 assert_eq!(split("helloworld"), &["helloworld"]);
17939 assert_eq!(
17940 split("this@is!@#$^many . symbols"),
17941 &[
17942 "symbols",
17943 " symbols",
17944 ". symbols",
17945 " . symbols",
17946 " . symbols",
17947 " . symbols",
17948 "many . symbols",
17949 "^many . symbols",
17950 "$^many . symbols",
17951 "#$^many . symbols",
17952 "@#$^many . symbols",
17953 "!@#$^many . symbols",
17954 "is!@#$^many . symbols",
17955 "@is!@#$^many . symbols",
17956 "this@is!@#$^many . symbols",
17957 ],
17958 );
17959 assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
17960}
17961
17962#[gpui::test]
17963async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17964 init_test(cx, |_| {});
17965
17966 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17967
17968 #[track_caller]
17969 fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
17970 let _state_context = cx.set_state(before);
17971 cx.run_until_parked();
17972 cx.update_editor(|editor, window, cx| {
17973 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17974 });
17975 cx.run_until_parked();
17976 cx.assert_editor_state(after);
17977 }
17978
17979 // Outside bracket jumps to outside of matching bracket
17980 assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
17981 assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
17982
17983 // Inside bracket jumps to inside of matching bracket
17984 assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
17985 assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
17986
17987 // When outside a bracket and inside, favor jumping to the inside bracket
17988 assert(
17989 "console.log('foo', [1, 2, 3]ˇ);",
17990 "console.log('foo', ˇ[1, 2, 3]);",
17991 &mut cx,
17992 );
17993 assert(
17994 "console.log(ˇ'foo', [1, 2, 3]);",
17995 "console.log('foo'ˇ, [1, 2, 3]);",
17996 &mut cx,
17997 );
17998
17999 // Bias forward if two options are equally likely
18000 assert(
18001 "let result = curried_fun()ˇ();",
18002 "let result = curried_fun()()ˇ;",
18003 &mut cx,
18004 );
18005
18006 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
18007 assert(
18008 indoc! {"
18009 function test() {
18010 console.log('test')ˇ
18011 }"},
18012 indoc! {"
18013 function test() {
18014 console.logˇ('test')
18015 }"},
18016 &mut cx,
18017 );
18018}
18019
18020#[gpui::test]
18021async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
18022 init_test(cx, |_| {});
18023 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
18024 language_registry.add(markdown_lang());
18025 language_registry.add(rust_lang());
18026 let buffer = cx.new(|cx| {
18027 let mut buffer = language::Buffer::local(
18028 indoc! {"
18029 ```rs
18030 impl Worktree {
18031 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18032 }
18033 }
18034 ```
18035 "},
18036 cx,
18037 );
18038 buffer.set_language_registry(language_registry.clone());
18039 buffer.set_language(Some(markdown_lang()), cx);
18040 buffer
18041 });
18042 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18043 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
18044 cx.executor().run_until_parked();
18045 _ = editor.update(cx, |editor, window, cx| {
18046 // Case 1: Test outer enclosing brackets
18047 select_ranges(
18048 editor,
18049 &indoc! {"
18050 ```rs
18051 impl Worktree {
18052 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18053 }
18054 }ˇ
18055 ```
18056 "},
18057 window,
18058 cx,
18059 );
18060 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
18061 assert_text_with_selections(
18062 editor,
18063 &indoc! {"
18064 ```rs
18065 impl Worktree ˇ{
18066 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18067 }
18068 }
18069 ```
18070 "},
18071 cx,
18072 );
18073 // Case 2: Test inner enclosing brackets
18074 select_ranges(
18075 editor,
18076 &indoc! {"
18077 ```rs
18078 impl Worktree {
18079 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18080 }ˇ
18081 }
18082 ```
18083 "},
18084 window,
18085 cx,
18086 );
18087 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
18088 assert_text_with_selections(
18089 editor,
18090 &indoc! {"
18091 ```rs
18092 impl Worktree {
18093 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
18094 }
18095 }
18096 ```
18097 "},
18098 cx,
18099 );
18100 });
18101}
18102
18103#[gpui::test]
18104async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
18105 init_test(cx, |_| {});
18106
18107 let fs = FakeFs::new(cx.executor());
18108 fs.insert_tree(
18109 path!("/a"),
18110 json!({
18111 "main.rs": "fn main() { let a = 5; }",
18112 "other.rs": "// Test file",
18113 }),
18114 )
18115 .await;
18116 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18117
18118 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18119 language_registry.add(Arc::new(Language::new(
18120 LanguageConfig {
18121 name: "Rust".into(),
18122 matcher: LanguageMatcher {
18123 path_suffixes: vec!["rs".to_string()],
18124 ..Default::default()
18125 },
18126 brackets: BracketPairConfig {
18127 pairs: vec![BracketPair {
18128 start: "{".to_string(),
18129 end: "}".to_string(),
18130 close: true,
18131 surround: true,
18132 newline: true,
18133 }],
18134 disabled_scopes_by_bracket_ix: Vec::new(),
18135 },
18136 ..Default::default()
18137 },
18138 Some(tree_sitter_rust::LANGUAGE.into()),
18139 )));
18140 let mut fake_servers = language_registry.register_fake_lsp(
18141 "Rust",
18142 FakeLspAdapter {
18143 capabilities: lsp::ServerCapabilities {
18144 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18145 first_trigger_character: "{".to_string(),
18146 more_trigger_character: None,
18147 }),
18148 ..Default::default()
18149 },
18150 ..Default::default()
18151 },
18152 );
18153
18154 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18155
18156 let cx = &mut VisualTestContext::from_window(*workspace, cx);
18157
18158 let worktree_id = workspace
18159 .update(cx, |workspace, _, cx| {
18160 workspace.project().update(cx, |project, cx| {
18161 project.worktrees(cx).next().unwrap().read(cx).id()
18162 })
18163 })
18164 .unwrap();
18165
18166 let buffer = project
18167 .update(cx, |project, cx| {
18168 project.open_local_buffer(path!("/a/main.rs"), cx)
18169 })
18170 .await
18171 .unwrap();
18172 let editor_handle = workspace
18173 .update(cx, |workspace, window, cx| {
18174 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
18175 })
18176 .unwrap()
18177 .await
18178 .unwrap()
18179 .downcast::<Editor>()
18180 .unwrap();
18181
18182 cx.executor().start_waiting();
18183 let fake_server = fake_servers.next().await.unwrap();
18184
18185 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
18186 |params, _| async move {
18187 assert_eq!(
18188 params.text_document_position.text_document.uri,
18189 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
18190 );
18191 assert_eq!(
18192 params.text_document_position.position,
18193 lsp::Position::new(0, 21),
18194 );
18195
18196 Ok(Some(vec![lsp::TextEdit {
18197 new_text: "]".to_string(),
18198 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18199 }]))
18200 },
18201 );
18202
18203 editor_handle.update_in(cx, |editor, window, cx| {
18204 window.focus(&editor.focus_handle(cx), cx);
18205 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18206 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
18207 });
18208 editor.handle_input("{", window, cx);
18209 });
18210
18211 cx.executor().run_until_parked();
18212
18213 buffer.update(cx, |buffer, _| {
18214 assert_eq!(
18215 buffer.text(),
18216 "fn main() { let a = {5}; }",
18217 "No extra braces from on type formatting should appear in the buffer"
18218 )
18219 });
18220}
18221
18222#[gpui::test(iterations = 20, seeds(31))]
18223async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
18224 init_test(cx, |_| {});
18225
18226 let mut cx = EditorLspTestContext::new_rust(
18227 lsp::ServerCapabilities {
18228 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18229 first_trigger_character: ".".to_string(),
18230 more_trigger_character: None,
18231 }),
18232 ..Default::default()
18233 },
18234 cx,
18235 )
18236 .await;
18237
18238 cx.update_buffer(|buffer, _| {
18239 // This causes autoindent to be async.
18240 buffer.set_sync_parse_timeout(Duration::ZERO)
18241 });
18242
18243 cx.set_state("fn c() {\n d()ˇ\n}\n");
18244 cx.simulate_keystroke("\n");
18245 cx.run_until_parked();
18246
18247 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
18248 let mut request =
18249 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
18250 let buffer_cloned = buffer_cloned.clone();
18251 async move {
18252 buffer_cloned.update(&mut cx, |buffer, _| {
18253 assert_eq!(
18254 buffer.text(),
18255 "fn c() {\n d()\n .\n}\n",
18256 "OnTypeFormatting should triggered after autoindent applied"
18257 )
18258 })?;
18259
18260 Ok(Some(vec![]))
18261 }
18262 });
18263
18264 cx.simulate_keystroke(".");
18265 cx.run_until_parked();
18266
18267 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
18268 assert!(request.next().await.is_some());
18269 request.close();
18270 assert!(request.next().await.is_none());
18271}
18272
18273#[gpui::test]
18274async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
18275 init_test(cx, |_| {});
18276
18277 let fs = FakeFs::new(cx.executor());
18278 fs.insert_tree(
18279 path!("/a"),
18280 json!({
18281 "main.rs": "fn main() { let a = 5; }",
18282 "other.rs": "// Test file",
18283 }),
18284 )
18285 .await;
18286
18287 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18288
18289 let server_restarts = Arc::new(AtomicUsize::new(0));
18290 let closure_restarts = Arc::clone(&server_restarts);
18291 let language_server_name = "test language server";
18292 let language_name: LanguageName = "Rust".into();
18293
18294 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18295 language_registry.add(Arc::new(Language::new(
18296 LanguageConfig {
18297 name: language_name.clone(),
18298 matcher: LanguageMatcher {
18299 path_suffixes: vec!["rs".to_string()],
18300 ..Default::default()
18301 },
18302 ..Default::default()
18303 },
18304 Some(tree_sitter_rust::LANGUAGE.into()),
18305 )));
18306 let mut fake_servers = language_registry.register_fake_lsp(
18307 "Rust",
18308 FakeLspAdapter {
18309 name: language_server_name,
18310 initialization_options: Some(json!({
18311 "testOptionValue": true
18312 })),
18313 initializer: Some(Box::new(move |fake_server| {
18314 let task_restarts = Arc::clone(&closure_restarts);
18315 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
18316 task_restarts.fetch_add(1, atomic::Ordering::Release);
18317 futures::future::ready(Ok(()))
18318 });
18319 })),
18320 ..Default::default()
18321 },
18322 );
18323
18324 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18325 let _buffer = project
18326 .update(cx, |project, cx| {
18327 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
18328 })
18329 .await
18330 .unwrap();
18331 let _fake_server = fake_servers.next().await.unwrap();
18332 update_test_language_settings(cx, |language_settings| {
18333 language_settings.languages.0.insert(
18334 language_name.clone().0,
18335 LanguageSettingsContent {
18336 tab_size: NonZeroU32::new(8),
18337 ..Default::default()
18338 },
18339 );
18340 });
18341 cx.executor().run_until_parked();
18342 assert_eq!(
18343 server_restarts.load(atomic::Ordering::Acquire),
18344 0,
18345 "Should not restart LSP server on an unrelated change"
18346 );
18347
18348 update_test_project_settings(cx, |project_settings| {
18349 project_settings.lsp.insert(
18350 "Some other server name".into(),
18351 LspSettings {
18352 binary: None,
18353 settings: None,
18354 initialization_options: Some(json!({
18355 "some other init value": false
18356 })),
18357 enable_lsp_tasks: false,
18358 fetch: None,
18359 },
18360 );
18361 });
18362 cx.executor().run_until_parked();
18363 assert_eq!(
18364 server_restarts.load(atomic::Ordering::Acquire),
18365 0,
18366 "Should not restart LSP server on an unrelated LSP settings change"
18367 );
18368
18369 update_test_project_settings(cx, |project_settings| {
18370 project_settings.lsp.insert(
18371 language_server_name.into(),
18372 LspSettings {
18373 binary: None,
18374 settings: None,
18375 initialization_options: Some(json!({
18376 "anotherInitValue": false
18377 })),
18378 enable_lsp_tasks: false,
18379 fetch: None,
18380 },
18381 );
18382 });
18383 cx.executor().run_until_parked();
18384 assert_eq!(
18385 server_restarts.load(atomic::Ordering::Acquire),
18386 1,
18387 "Should restart LSP server on a related LSP settings change"
18388 );
18389
18390 update_test_project_settings(cx, |project_settings| {
18391 project_settings.lsp.insert(
18392 language_server_name.into(),
18393 LspSettings {
18394 binary: None,
18395 settings: None,
18396 initialization_options: Some(json!({
18397 "anotherInitValue": false
18398 })),
18399 enable_lsp_tasks: false,
18400 fetch: None,
18401 },
18402 );
18403 });
18404 cx.executor().run_until_parked();
18405 assert_eq!(
18406 server_restarts.load(atomic::Ordering::Acquire),
18407 1,
18408 "Should not restart LSP server on a related LSP settings change that is the same"
18409 );
18410
18411 update_test_project_settings(cx, |project_settings| {
18412 project_settings.lsp.insert(
18413 language_server_name.into(),
18414 LspSettings {
18415 binary: None,
18416 settings: None,
18417 initialization_options: None,
18418 enable_lsp_tasks: false,
18419 fetch: None,
18420 },
18421 );
18422 });
18423 cx.executor().run_until_parked();
18424 assert_eq!(
18425 server_restarts.load(atomic::Ordering::Acquire),
18426 2,
18427 "Should restart LSP server on another related LSP settings change"
18428 );
18429}
18430
18431#[gpui::test]
18432async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
18433 init_test(cx, |_| {});
18434
18435 let mut cx = EditorLspTestContext::new_rust(
18436 lsp::ServerCapabilities {
18437 completion_provider: Some(lsp::CompletionOptions {
18438 trigger_characters: Some(vec![".".to_string()]),
18439 resolve_provider: Some(true),
18440 ..Default::default()
18441 }),
18442 ..Default::default()
18443 },
18444 cx,
18445 )
18446 .await;
18447
18448 cx.set_state("fn main() { let a = 2ˇ; }");
18449 cx.simulate_keystroke(".");
18450 let completion_item = lsp::CompletionItem {
18451 label: "some".into(),
18452 kind: Some(lsp::CompletionItemKind::SNIPPET),
18453 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
18454 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
18455 kind: lsp::MarkupKind::Markdown,
18456 value: "```rust\nSome(2)\n```".to_string(),
18457 })),
18458 deprecated: Some(false),
18459 sort_text: Some("fffffff2".to_string()),
18460 filter_text: Some("some".to_string()),
18461 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
18462 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18463 range: lsp::Range {
18464 start: lsp::Position {
18465 line: 0,
18466 character: 22,
18467 },
18468 end: lsp::Position {
18469 line: 0,
18470 character: 22,
18471 },
18472 },
18473 new_text: "Some(2)".to_string(),
18474 })),
18475 additional_text_edits: Some(vec![lsp::TextEdit {
18476 range: lsp::Range {
18477 start: lsp::Position {
18478 line: 0,
18479 character: 20,
18480 },
18481 end: lsp::Position {
18482 line: 0,
18483 character: 22,
18484 },
18485 },
18486 new_text: "".to_string(),
18487 }]),
18488 ..Default::default()
18489 };
18490
18491 let closure_completion_item = completion_item.clone();
18492 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18493 let task_completion_item = closure_completion_item.clone();
18494 async move {
18495 Ok(Some(lsp::CompletionResponse::Array(vec![
18496 task_completion_item,
18497 ])))
18498 }
18499 });
18500
18501 request.next().await;
18502
18503 cx.condition(|editor, _| editor.context_menu_visible())
18504 .await;
18505 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
18506 editor
18507 .confirm_completion(&ConfirmCompletion::default(), window, cx)
18508 .unwrap()
18509 });
18510 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
18511
18512 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18513 let task_completion_item = completion_item.clone();
18514 async move { Ok(task_completion_item) }
18515 })
18516 .next()
18517 .await
18518 .unwrap();
18519 apply_additional_edits.await.unwrap();
18520 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
18521}
18522
18523#[gpui::test]
18524async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
18525 init_test(cx, |_| {});
18526
18527 let mut cx = EditorLspTestContext::new_rust(
18528 lsp::ServerCapabilities {
18529 completion_provider: Some(lsp::CompletionOptions {
18530 trigger_characters: Some(vec![".".to_string()]),
18531 resolve_provider: Some(true),
18532 ..Default::default()
18533 }),
18534 ..Default::default()
18535 },
18536 cx,
18537 )
18538 .await;
18539
18540 cx.set_state("fn main() { let a = 2ˇ; }");
18541 cx.simulate_keystroke(".");
18542
18543 let item1 = lsp::CompletionItem {
18544 label: "method id()".to_string(),
18545 filter_text: Some("id".to_string()),
18546 detail: None,
18547 documentation: None,
18548 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18549 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18550 new_text: ".id".to_string(),
18551 })),
18552 ..lsp::CompletionItem::default()
18553 };
18554
18555 let item2 = lsp::CompletionItem {
18556 label: "other".to_string(),
18557 filter_text: Some("other".to_string()),
18558 detail: None,
18559 documentation: None,
18560 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18561 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18562 new_text: ".other".to_string(),
18563 })),
18564 ..lsp::CompletionItem::default()
18565 };
18566
18567 let item1 = item1.clone();
18568 cx.set_request_handler::<lsp::request::Completion, _, _>({
18569 let item1 = item1.clone();
18570 move |_, _, _| {
18571 let item1 = item1.clone();
18572 let item2 = item2.clone();
18573 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
18574 }
18575 })
18576 .next()
18577 .await;
18578
18579 cx.condition(|editor, _| editor.context_menu_visible())
18580 .await;
18581 cx.update_editor(|editor, _, _| {
18582 let context_menu = editor.context_menu.borrow_mut();
18583 let context_menu = context_menu
18584 .as_ref()
18585 .expect("Should have the context menu deployed");
18586 match context_menu {
18587 CodeContextMenu::Completions(completions_menu) => {
18588 let completions = completions_menu.completions.borrow_mut();
18589 assert_eq!(
18590 completions
18591 .iter()
18592 .map(|completion| &completion.label.text)
18593 .collect::<Vec<_>>(),
18594 vec!["method id()", "other"]
18595 )
18596 }
18597 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18598 }
18599 });
18600
18601 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
18602 let item1 = item1.clone();
18603 move |_, item_to_resolve, _| {
18604 let item1 = item1.clone();
18605 async move {
18606 if item1 == item_to_resolve {
18607 Ok(lsp::CompletionItem {
18608 label: "method id()".to_string(),
18609 filter_text: Some("id".to_string()),
18610 detail: Some("Now resolved!".to_string()),
18611 documentation: Some(lsp::Documentation::String("Docs".to_string())),
18612 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18613 range: lsp::Range::new(
18614 lsp::Position::new(0, 22),
18615 lsp::Position::new(0, 22),
18616 ),
18617 new_text: ".id".to_string(),
18618 })),
18619 ..lsp::CompletionItem::default()
18620 })
18621 } else {
18622 Ok(item_to_resolve)
18623 }
18624 }
18625 }
18626 })
18627 .next()
18628 .await
18629 .unwrap();
18630 cx.run_until_parked();
18631
18632 cx.update_editor(|editor, window, cx| {
18633 editor.context_menu_next(&Default::default(), window, cx);
18634 });
18635
18636 cx.update_editor(|editor, _, _| {
18637 let context_menu = editor.context_menu.borrow_mut();
18638 let context_menu = context_menu
18639 .as_ref()
18640 .expect("Should have the context menu deployed");
18641 match context_menu {
18642 CodeContextMenu::Completions(completions_menu) => {
18643 let completions = completions_menu.completions.borrow_mut();
18644 assert_eq!(
18645 completions
18646 .iter()
18647 .map(|completion| &completion.label.text)
18648 .collect::<Vec<_>>(),
18649 vec!["method id() Now resolved!", "other"],
18650 "Should update first completion label, but not second as the filter text did not match."
18651 );
18652 }
18653 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18654 }
18655 });
18656}
18657
18658#[gpui::test]
18659async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
18660 init_test(cx, |_| {});
18661 let mut cx = EditorLspTestContext::new_rust(
18662 lsp::ServerCapabilities {
18663 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
18664 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
18665 completion_provider: Some(lsp::CompletionOptions {
18666 resolve_provider: Some(true),
18667 ..Default::default()
18668 }),
18669 ..Default::default()
18670 },
18671 cx,
18672 )
18673 .await;
18674 cx.set_state(indoc! {"
18675 struct TestStruct {
18676 field: i32
18677 }
18678
18679 fn mainˇ() {
18680 let unused_var = 42;
18681 let test_struct = TestStruct { field: 42 };
18682 }
18683 "});
18684 let symbol_range = cx.lsp_range(indoc! {"
18685 struct TestStruct {
18686 field: i32
18687 }
18688
18689 «fn main»() {
18690 let unused_var = 42;
18691 let test_struct = TestStruct { field: 42 };
18692 }
18693 "});
18694 let mut hover_requests =
18695 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
18696 Ok(Some(lsp::Hover {
18697 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
18698 kind: lsp::MarkupKind::Markdown,
18699 value: "Function documentation".to_string(),
18700 }),
18701 range: Some(symbol_range),
18702 }))
18703 });
18704
18705 // Case 1: Test that code action menu hide hover popover
18706 cx.dispatch_action(Hover);
18707 hover_requests.next().await;
18708 cx.condition(|editor, _| editor.hover_state.visible()).await;
18709 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
18710 move |_, _, _| async move {
18711 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
18712 lsp::CodeAction {
18713 title: "Remove unused variable".to_string(),
18714 kind: Some(CodeActionKind::QUICKFIX),
18715 edit: Some(lsp::WorkspaceEdit {
18716 changes: Some(
18717 [(
18718 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
18719 vec![lsp::TextEdit {
18720 range: lsp::Range::new(
18721 lsp::Position::new(5, 4),
18722 lsp::Position::new(5, 27),
18723 ),
18724 new_text: "".to_string(),
18725 }],
18726 )]
18727 .into_iter()
18728 .collect(),
18729 ),
18730 ..Default::default()
18731 }),
18732 ..Default::default()
18733 },
18734 )]))
18735 },
18736 );
18737 cx.update_editor(|editor, window, cx| {
18738 editor.toggle_code_actions(
18739 &ToggleCodeActions {
18740 deployed_from: None,
18741 quick_launch: false,
18742 },
18743 window,
18744 cx,
18745 );
18746 });
18747 code_action_requests.next().await;
18748 cx.run_until_parked();
18749 cx.condition(|editor, _| editor.context_menu_visible())
18750 .await;
18751 cx.update_editor(|editor, _, _| {
18752 assert!(
18753 !editor.hover_state.visible(),
18754 "Hover popover should be hidden when code action menu is shown"
18755 );
18756 // Hide code actions
18757 editor.context_menu.take();
18758 });
18759
18760 // Case 2: Test that code completions hide hover popover
18761 cx.dispatch_action(Hover);
18762 hover_requests.next().await;
18763 cx.condition(|editor, _| editor.hover_state.visible()).await;
18764 let counter = Arc::new(AtomicUsize::new(0));
18765 let mut completion_requests =
18766 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18767 let counter = counter.clone();
18768 async move {
18769 counter.fetch_add(1, atomic::Ordering::Release);
18770 Ok(Some(lsp::CompletionResponse::Array(vec![
18771 lsp::CompletionItem {
18772 label: "main".into(),
18773 kind: Some(lsp::CompletionItemKind::FUNCTION),
18774 detail: Some("() -> ()".to_string()),
18775 ..Default::default()
18776 },
18777 lsp::CompletionItem {
18778 label: "TestStruct".into(),
18779 kind: Some(lsp::CompletionItemKind::STRUCT),
18780 detail: Some("struct TestStruct".to_string()),
18781 ..Default::default()
18782 },
18783 ])))
18784 }
18785 });
18786 cx.update_editor(|editor, window, cx| {
18787 editor.show_completions(&ShowCompletions, window, cx);
18788 });
18789 completion_requests.next().await;
18790 cx.condition(|editor, _| editor.context_menu_visible())
18791 .await;
18792 cx.update_editor(|editor, _, _| {
18793 assert!(
18794 !editor.hover_state.visible(),
18795 "Hover popover should be hidden when completion menu is shown"
18796 );
18797 });
18798}
18799
18800#[gpui::test]
18801async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
18802 init_test(cx, |_| {});
18803
18804 let mut cx = EditorLspTestContext::new_rust(
18805 lsp::ServerCapabilities {
18806 completion_provider: Some(lsp::CompletionOptions {
18807 trigger_characters: Some(vec![".".to_string()]),
18808 resolve_provider: Some(true),
18809 ..Default::default()
18810 }),
18811 ..Default::default()
18812 },
18813 cx,
18814 )
18815 .await;
18816
18817 cx.set_state("fn main() { let a = 2ˇ; }");
18818 cx.simulate_keystroke(".");
18819
18820 let unresolved_item_1 = lsp::CompletionItem {
18821 label: "id".to_string(),
18822 filter_text: Some("id".to_string()),
18823 detail: None,
18824 documentation: None,
18825 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18826 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18827 new_text: ".id".to_string(),
18828 })),
18829 ..lsp::CompletionItem::default()
18830 };
18831 let resolved_item_1 = lsp::CompletionItem {
18832 additional_text_edits: Some(vec![lsp::TextEdit {
18833 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18834 new_text: "!!".to_string(),
18835 }]),
18836 ..unresolved_item_1.clone()
18837 };
18838 let unresolved_item_2 = lsp::CompletionItem {
18839 label: "other".to_string(),
18840 filter_text: Some("other".to_string()),
18841 detail: None,
18842 documentation: None,
18843 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18844 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18845 new_text: ".other".to_string(),
18846 })),
18847 ..lsp::CompletionItem::default()
18848 };
18849 let resolved_item_2 = lsp::CompletionItem {
18850 additional_text_edits: Some(vec![lsp::TextEdit {
18851 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18852 new_text: "??".to_string(),
18853 }]),
18854 ..unresolved_item_2.clone()
18855 };
18856
18857 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
18858 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
18859 cx.lsp
18860 .server
18861 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18862 let unresolved_item_1 = unresolved_item_1.clone();
18863 let resolved_item_1 = resolved_item_1.clone();
18864 let unresolved_item_2 = unresolved_item_2.clone();
18865 let resolved_item_2 = resolved_item_2.clone();
18866 let resolve_requests_1 = resolve_requests_1.clone();
18867 let resolve_requests_2 = resolve_requests_2.clone();
18868 move |unresolved_request, _| {
18869 let unresolved_item_1 = unresolved_item_1.clone();
18870 let resolved_item_1 = resolved_item_1.clone();
18871 let unresolved_item_2 = unresolved_item_2.clone();
18872 let resolved_item_2 = resolved_item_2.clone();
18873 let resolve_requests_1 = resolve_requests_1.clone();
18874 let resolve_requests_2 = resolve_requests_2.clone();
18875 async move {
18876 if unresolved_request == unresolved_item_1 {
18877 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
18878 Ok(resolved_item_1.clone())
18879 } else if unresolved_request == unresolved_item_2 {
18880 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
18881 Ok(resolved_item_2.clone())
18882 } else {
18883 panic!("Unexpected completion item {unresolved_request:?}")
18884 }
18885 }
18886 }
18887 })
18888 .detach();
18889
18890 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18891 let unresolved_item_1 = unresolved_item_1.clone();
18892 let unresolved_item_2 = unresolved_item_2.clone();
18893 async move {
18894 Ok(Some(lsp::CompletionResponse::Array(vec![
18895 unresolved_item_1,
18896 unresolved_item_2,
18897 ])))
18898 }
18899 })
18900 .next()
18901 .await;
18902
18903 cx.condition(|editor, _| editor.context_menu_visible())
18904 .await;
18905 cx.update_editor(|editor, _, _| {
18906 let context_menu = editor.context_menu.borrow_mut();
18907 let context_menu = context_menu
18908 .as_ref()
18909 .expect("Should have the context menu deployed");
18910 match context_menu {
18911 CodeContextMenu::Completions(completions_menu) => {
18912 let completions = completions_menu.completions.borrow_mut();
18913 assert_eq!(
18914 completions
18915 .iter()
18916 .map(|completion| &completion.label.text)
18917 .collect::<Vec<_>>(),
18918 vec!["id", "other"]
18919 )
18920 }
18921 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18922 }
18923 });
18924 cx.run_until_parked();
18925
18926 cx.update_editor(|editor, window, cx| {
18927 editor.context_menu_next(&ContextMenuNext, window, cx);
18928 });
18929 cx.run_until_parked();
18930 cx.update_editor(|editor, window, cx| {
18931 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18932 });
18933 cx.run_until_parked();
18934 cx.update_editor(|editor, window, cx| {
18935 editor.context_menu_next(&ContextMenuNext, window, cx);
18936 });
18937 cx.run_until_parked();
18938 cx.update_editor(|editor, window, cx| {
18939 editor
18940 .compose_completion(&ComposeCompletion::default(), window, cx)
18941 .expect("No task returned")
18942 })
18943 .await
18944 .expect("Completion failed");
18945 cx.run_until_parked();
18946
18947 cx.update_editor(|editor, _, cx| {
18948 assert_eq!(
18949 resolve_requests_1.load(atomic::Ordering::Acquire),
18950 1,
18951 "Should always resolve once despite multiple selections"
18952 );
18953 assert_eq!(
18954 resolve_requests_2.load(atomic::Ordering::Acquire),
18955 1,
18956 "Should always resolve once after multiple selections and applying the completion"
18957 );
18958 assert_eq!(
18959 editor.text(cx),
18960 "fn main() { let a = ??.other; }",
18961 "Should use resolved data when applying the completion"
18962 );
18963 });
18964}
18965
18966#[gpui::test]
18967async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18968 init_test(cx, |_| {});
18969
18970 let item_0 = lsp::CompletionItem {
18971 label: "abs".into(),
18972 insert_text: Some("abs".into()),
18973 data: Some(json!({ "very": "special"})),
18974 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18975 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18976 lsp::InsertReplaceEdit {
18977 new_text: "abs".to_string(),
18978 insert: lsp::Range::default(),
18979 replace: lsp::Range::default(),
18980 },
18981 )),
18982 ..lsp::CompletionItem::default()
18983 };
18984 let items = iter::once(item_0.clone())
18985 .chain((11..51).map(|i| lsp::CompletionItem {
18986 label: format!("item_{}", i),
18987 insert_text: Some(format!("item_{}", i)),
18988 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18989 ..lsp::CompletionItem::default()
18990 }))
18991 .collect::<Vec<_>>();
18992
18993 let default_commit_characters = vec!["?".to_string()];
18994 let default_data = json!({ "default": "data"});
18995 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18996 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18997 let default_edit_range = lsp::Range {
18998 start: lsp::Position {
18999 line: 0,
19000 character: 5,
19001 },
19002 end: lsp::Position {
19003 line: 0,
19004 character: 5,
19005 },
19006 };
19007
19008 let mut cx = EditorLspTestContext::new_rust(
19009 lsp::ServerCapabilities {
19010 completion_provider: Some(lsp::CompletionOptions {
19011 trigger_characters: Some(vec![".".to_string()]),
19012 resolve_provider: Some(true),
19013 ..Default::default()
19014 }),
19015 ..Default::default()
19016 },
19017 cx,
19018 )
19019 .await;
19020
19021 cx.set_state("fn main() { let a = 2ˇ; }");
19022 cx.simulate_keystroke(".");
19023
19024 let completion_data = default_data.clone();
19025 let completion_characters = default_commit_characters.clone();
19026 let completion_items = items.clone();
19027 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19028 let default_data = completion_data.clone();
19029 let default_commit_characters = completion_characters.clone();
19030 let items = completion_items.clone();
19031 async move {
19032 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
19033 items,
19034 item_defaults: Some(lsp::CompletionListItemDefaults {
19035 data: Some(default_data.clone()),
19036 commit_characters: Some(default_commit_characters.clone()),
19037 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
19038 default_edit_range,
19039 )),
19040 insert_text_format: Some(default_insert_text_format),
19041 insert_text_mode: Some(default_insert_text_mode),
19042 }),
19043 ..lsp::CompletionList::default()
19044 })))
19045 }
19046 })
19047 .next()
19048 .await;
19049
19050 let resolved_items = Arc::new(Mutex::new(Vec::new()));
19051 cx.lsp
19052 .server
19053 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
19054 let closure_resolved_items = resolved_items.clone();
19055 move |item_to_resolve, _| {
19056 let closure_resolved_items = closure_resolved_items.clone();
19057 async move {
19058 closure_resolved_items.lock().push(item_to_resolve.clone());
19059 Ok(item_to_resolve)
19060 }
19061 }
19062 })
19063 .detach();
19064
19065 cx.condition(|editor, _| editor.context_menu_visible())
19066 .await;
19067 cx.run_until_parked();
19068 cx.update_editor(|editor, _, _| {
19069 let menu = editor.context_menu.borrow_mut();
19070 match menu.as_ref().expect("should have the completions menu") {
19071 CodeContextMenu::Completions(completions_menu) => {
19072 assert_eq!(
19073 completions_menu
19074 .entries
19075 .borrow()
19076 .iter()
19077 .map(|mat| mat.string.clone())
19078 .collect::<Vec<String>>(),
19079 items
19080 .iter()
19081 .map(|completion| completion.label.clone())
19082 .collect::<Vec<String>>()
19083 );
19084 }
19085 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
19086 }
19087 });
19088 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
19089 // with 4 from the end.
19090 assert_eq!(
19091 *resolved_items.lock(),
19092 [&items[0..16], &items[items.len() - 4..items.len()]]
19093 .concat()
19094 .iter()
19095 .cloned()
19096 .map(|mut item| {
19097 if item.data.is_none() {
19098 item.data = Some(default_data.clone());
19099 }
19100 item
19101 })
19102 .collect::<Vec<lsp::CompletionItem>>(),
19103 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
19104 );
19105 resolved_items.lock().clear();
19106
19107 cx.update_editor(|editor, window, cx| {
19108 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
19109 });
19110 cx.run_until_parked();
19111 // Completions that have already been resolved are skipped.
19112 assert_eq!(
19113 *resolved_items.lock(),
19114 items[items.len() - 17..items.len() - 4]
19115 .iter()
19116 .cloned()
19117 .map(|mut item| {
19118 if item.data.is_none() {
19119 item.data = Some(default_data.clone());
19120 }
19121 item
19122 })
19123 .collect::<Vec<lsp::CompletionItem>>()
19124 );
19125 resolved_items.lock().clear();
19126}
19127
19128#[gpui::test]
19129async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
19130 init_test(cx, |_| {});
19131
19132 let mut cx = EditorLspTestContext::new(
19133 Language::new(
19134 LanguageConfig {
19135 matcher: LanguageMatcher {
19136 path_suffixes: vec!["jsx".into()],
19137 ..Default::default()
19138 },
19139 overrides: [(
19140 "element".into(),
19141 LanguageConfigOverride {
19142 completion_query_characters: Override::Set(['-'].into_iter().collect()),
19143 ..Default::default()
19144 },
19145 )]
19146 .into_iter()
19147 .collect(),
19148 ..Default::default()
19149 },
19150 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
19151 )
19152 .with_override_query("(jsx_self_closing_element) @element")
19153 .unwrap(),
19154 lsp::ServerCapabilities {
19155 completion_provider: Some(lsp::CompletionOptions {
19156 trigger_characters: Some(vec![":".to_string()]),
19157 ..Default::default()
19158 }),
19159 ..Default::default()
19160 },
19161 cx,
19162 )
19163 .await;
19164
19165 cx.lsp
19166 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
19167 Ok(Some(lsp::CompletionResponse::Array(vec![
19168 lsp::CompletionItem {
19169 label: "bg-blue".into(),
19170 ..Default::default()
19171 },
19172 lsp::CompletionItem {
19173 label: "bg-red".into(),
19174 ..Default::default()
19175 },
19176 lsp::CompletionItem {
19177 label: "bg-yellow".into(),
19178 ..Default::default()
19179 },
19180 ])))
19181 });
19182
19183 cx.set_state(r#"<p class="bgˇ" />"#);
19184
19185 // Trigger completion when typing a dash, because the dash is an extra
19186 // word character in the 'element' scope, which contains the cursor.
19187 cx.simulate_keystroke("-");
19188 cx.executor().run_until_parked();
19189 cx.update_editor(|editor, _, _| {
19190 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19191 {
19192 assert_eq!(
19193 completion_menu_entries(menu),
19194 &["bg-blue", "bg-red", "bg-yellow"]
19195 );
19196 } else {
19197 panic!("expected completion menu to be open");
19198 }
19199 });
19200
19201 cx.simulate_keystroke("l");
19202 cx.executor().run_until_parked();
19203 cx.update_editor(|editor, _, _| {
19204 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19205 {
19206 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
19207 } else {
19208 panic!("expected completion menu to be open");
19209 }
19210 });
19211
19212 // When filtering completions, consider the character after the '-' to
19213 // be the start of a subword.
19214 cx.set_state(r#"<p class="yelˇ" />"#);
19215 cx.simulate_keystroke("l");
19216 cx.executor().run_until_parked();
19217 cx.update_editor(|editor, _, _| {
19218 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19219 {
19220 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
19221 } else {
19222 panic!("expected completion menu to be open");
19223 }
19224 });
19225}
19226
19227fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
19228 let entries = menu.entries.borrow();
19229 entries.iter().map(|mat| mat.string.clone()).collect()
19230}
19231
19232#[gpui::test]
19233async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
19234 init_test(cx, |settings| {
19235 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19236 });
19237
19238 let fs = FakeFs::new(cx.executor());
19239 fs.insert_file(path!("/file.ts"), Default::default()).await;
19240
19241 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
19242 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19243
19244 language_registry.add(Arc::new(Language::new(
19245 LanguageConfig {
19246 name: "TypeScript".into(),
19247 matcher: LanguageMatcher {
19248 path_suffixes: vec!["ts".to_string()],
19249 ..Default::default()
19250 },
19251 ..Default::default()
19252 },
19253 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19254 )));
19255 update_test_language_settings(cx, |settings| {
19256 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19257 });
19258
19259 let test_plugin = "test_plugin";
19260 let _ = language_registry.register_fake_lsp(
19261 "TypeScript",
19262 FakeLspAdapter {
19263 prettier_plugins: vec![test_plugin],
19264 ..Default::default()
19265 },
19266 );
19267
19268 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19269 let buffer = project
19270 .update(cx, |project, cx| {
19271 project.open_local_buffer(path!("/file.ts"), cx)
19272 })
19273 .await
19274 .unwrap();
19275
19276 let buffer_text = "one\ntwo\nthree\n";
19277 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19278 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19279 editor.update_in(cx, |editor, window, cx| {
19280 editor.set_text(buffer_text, window, cx)
19281 });
19282
19283 editor
19284 .update_in(cx, |editor, window, cx| {
19285 editor.perform_format(
19286 project.clone(),
19287 FormatTrigger::Manual,
19288 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19289 window,
19290 cx,
19291 )
19292 })
19293 .unwrap()
19294 .await;
19295 assert_eq!(
19296 editor.update(cx, |editor, cx| editor.text(cx)),
19297 buffer_text.to_string() + prettier_format_suffix,
19298 "Test prettier formatting was not applied to the original buffer text",
19299 );
19300
19301 update_test_language_settings(cx, |settings| {
19302 settings.defaults.formatter = Some(FormatterList::default())
19303 });
19304 let format = editor.update_in(cx, |editor, window, cx| {
19305 editor.perform_format(
19306 project.clone(),
19307 FormatTrigger::Manual,
19308 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19309 window,
19310 cx,
19311 )
19312 });
19313 format.await.unwrap();
19314 assert_eq!(
19315 editor.update(cx, |editor, cx| editor.text(cx)),
19316 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
19317 "Autoformatting (via test prettier) was not applied to the original buffer text",
19318 );
19319}
19320
19321#[gpui::test]
19322async fn test_document_format_with_prettier_explicit_language(cx: &mut TestAppContext) {
19323 init_test(cx, |settings| {
19324 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19325 });
19326
19327 let fs = FakeFs::new(cx.executor());
19328 fs.insert_file(path!("/file.settings"), Default::default())
19329 .await;
19330
19331 let project = Project::test(fs, [path!("/file.settings").as_ref()], cx).await;
19332 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19333
19334 let ts_lang = Arc::new(Language::new(
19335 LanguageConfig {
19336 name: "TypeScript".into(),
19337 matcher: LanguageMatcher {
19338 path_suffixes: vec!["ts".to_string()],
19339 ..LanguageMatcher::default()
19340 },
19341 prettier_parser_name: Some("typescript".to_string()),
19342 ..LanguageConfig::default()
19343 },
19344 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19345 ));
19346
19347 language_registry.add(ts_lang.clone());
19348
19349 update_test_language_settings(cx, |settings| {
19350 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19351 });
19352
19353 let test_plugin = "test_plugin";
19354 let _ = language_registry.register_fake_lsp(
19355 "TypeScript",
19356 FakeLspAdapter {
19357 prettier_plugins: vec![test_plugin],
19358 ..Default::default()
19359 },
19360 );
19361
19362 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19363 let buffer = project
19364 .update(cx, |project, cx| {
19365 project.open_local_buffer(path!("/file.settings"), cx)
19366 })
19367 .await
19368 .unwrap();
19369
19370 project.update(cx, |project, cx| {
19371 project.set_language_for_buffer(&buffer, ts_lang, cx)
19372 });
19373
19374 let buffer_text = "one\ntwo\nthree\n";
19375 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19376 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19377 editor.update_in(cx, |editor, window, cx| {
19378 editor.set_text(buffer_text, window, cx)
19379 });
19380
19381 editor
19382 .update_in(cx, |editor, window, cx| {
19383 editor.perform_format(
19384 project.clone(),
19385 FormatTrigger::Manual,
19386 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19387 window,
19388 cx,
19389 )
19390 })
19391 .unwrap()
19392 .await;
19393 assert_eq!(
19394 editor.update(cx, |editor, cx| editor.text(cx)),
19395 buffer_text.to_string() + prettier_format_suffix + "\ntypescript",
19396 "Test prettier formatting was not applied to the original buffer text",
19397 );
19398
19399 update_test_language_settings(cx, |settings| {
19400 settings.defaults.formatter = Some(FormatterList::default())
19401 });
19402 let format = editor.update_in(cx, |editor, window, cx| {
19403 editor.perform_format(
19404 project.clone(),
19405 FormatTrigger::Manual,
19406 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19407 window,
19408 cx,
19409 )
19410 });
19411 format.await.unwrap();
19412
19413 assert_eq!(
19414 editor.update(cx, |editor, cx| editor.text(cx)),
19415 buffer_text.to_string()
19416 + prettier_format_suffix
19417 + "\ntypescript\n"
19418 + prettier_format_suffix
19419 + "\ntypescript",
19420 "Autoformatting (via test prettier) was not applied to the original buffer text",
19421 );
19422}
19423
19424#[gpui::test]
19425async fn test_addition_reverts(cx: &mut TestAppContext) {
19426 init_test(cx, |_| {});
19427 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19428 let base_text = indoc! {r#"
19429 struct Row;
19430 struct Row1;
19431 struct Row2;
19432
19433 struct Row4;
19434 struct Row5;
19435 struct Row6;
19436
19437 struct Row8;
19438 struct Row9;
19439 struct Row10;"#};
19440
19441 // When addition hunks are not adjacent to carets, no hunk revert is performed
19442 assert_hunk_revert(
19443 indoc! {r#"struct Row;
19444 struct Row1;
19445 struct Row1.1;
19446 struct Row1.2;
19447 struct Row2;ˇ
19448
19449 struct Row4;
19450 struct Row5;
19451 struct Row6;
19452
19453 struct Row8;
19454 ˇstruct Row9;
19455 struct Row9.1;
19456 struct Row9.2;
19457 struct Row9.3;
19458 struct Row10;"#},
19459 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19460 indoc! {r#"struct Row;
19461 struct Row1;
19462 struct Row1.1;
19463 struct Row1.2;
19464 struct Row2;ˇ
19465
19466 struct Row4;
19467 struct Row5;
19468 struct Row6;
19469
19470 struct Row8;
19471 ˇstruct Row9;
19472 struct Row9.1;
19473 struct Row9.2;
19474 struct Row9.3;
19475 struct Row10;"#},
19476 base_text,
19477 &mut cx,
19478 );
19479 // Same for selections
19480 assert_hunk_revert(
19481 indoc! {r#"struct Row;
19482 struct Row1;
19483 struct Row2;
19484 struct Row2.1;
19485 struct Row2.2;
19486 «ˇ
19487 struct Row4;
19488 struct» Row5;
19489 «struct Row6;
19490 ˇ»
19491 struct Row9.1;
19492 struct Row9.2;
19493 struct Row9.3;
19494 struct Row8;
19495 struct Row9;
19496 struct Row10;"#},
19497 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19498 indoc! {r#"struct Row;
19499 struct Row1;
19500 struct Row2;
19501 struct Row2.1;
19502 struct Row2.2;
19503 «ˇ
19504 struct Row4;
19505 struct» Row5;
19506 «struct Row6;
19507 ˇ»
19508 struct Row9.1;
19509 struct Row9.2;
19510 struct Row9.3;
19511 struct Row8;
19512 struct Row9;
19513 struct Row10;"#},
19514 base_text,
19515 &mut cx,
19516 );
19517
19518 // When carets and selections intersect the addition hunks, those are reverted.
19519 // Adjacent carets got merged.
19520 assert_hunk_revert(
19521 indoc! {r#"struct Row;
19522 ˇ// something on the top
19523 struct Row1;
19524 struct Row2;
19525 struct Roˇw3.1;
19526 struct Row2.2;
19527 struct Row2.3;ˇ
19528
19529 struct Row4;
19530 struct ˇRow5.1;
19531 struct Row5.2;
19532 struct «Rowˇ»5.3;
19533 struct Row5;
19534 struct Row6;
19535 ˇ
19536 struct Row9.1;
19537 struct «Rowˇ»9.2;
19538 struct «ˇRow»9.3;
19539 struct Row8;
19540 struct Row9;
19541 «ˇ// something on bottom»
19542 struct Row10;"#},
19543 vec![
19544 DiffHunkStatusKind::Added,
19545 DiffHunkStatusKind::Added,
19546 DiffHunkStatusKind::Added,
19547 DiffHunkStatusKind::Added,
19548 DiffHunkStatusKind::Added,
19549 ],
19550 indoc! {r#"struct Row;
19551 ˇstruct Row1;
19552 struct Row2;
19553 ˇ
19554 struct Row4;
19555 ˇstruct Row5;
19556 struct Row6;
19557 ˇ
19558 ˇstruct Row8;
19559 struct Row9;
19560 ˇstruct Row10;"#},
19561 base_text,
19562 &mut cx,
19563 );
19564}
19565
19566#[gpui::test]
19567async fn test_modification_reverts(cx: &mut TestAppContext) {
19568 init_test(cx, |_| {});
19569 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19570 let base_text = indoc! {r#"
19571 struct Row;
19572 struct Row1;
19573 struct Row2;
19574
19575 struct Row4;
19576 struct Row5;
19577 struct Row6;
19578
19579 struct Row8;
19580 struct Row9;
19581 struct Row10;"#};
19582
19583 // Modification hunks behave the same as the addition ones.
19584 assert_hunk_revert(
19585 indoc! {r#"struct Row;
19586 struct Row1;
19587 struct Row33;
19588 ˇ
19589 struct Row4;
19590 struct Row5;
19591 struct Row6;
19592 ˇ
19593 struct Row99;
19594 struct Row9;
19595 struct Row10;"#},
19596 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19597 indoc! {r#"struct Row;
19598 struct Row1;
19599 struct Row33;
19600 ˇ
19601 struct Row4;
19602 struct Row5;
19603 struct Row6;
19604 ˇ
19605 struct Row99;
19606 struct Row9;
19607 struct Row10;"#},
19608 base_text,
19609 &mut cx,
19610 );
19611 assert_hunk_revert(
19612 indoc! {r#"struct Row;
19613 struct Row1;
19614 struct Row33;
19615 «ˇ
19616 struct Row4;
19617 struct» Row5;
19618 «struct Row6;
19619 ˇ»
19620 struct Row99;
19621 struct Row9;
19622 struct Row10;"#},
19623 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
19624 indoc! {r#"struct Row;
19625 struct Row1;
19626 struct Row33;
19627 «ˇ
19628 struct Row4;
19629 struct» Row5;
19630 «struct Row6;
19631 ˇ»
19632 struct Row99;
19633 struct Row9;
19634 struct Row10;"#},
19635 base_text,
19636 &mut cx,
19637 );
19638
19639 assert_hunk_revert(
19640 indoc! {r#"ˇstruct Row1.1;
19641 struct Row1;
19642 «ˇstr»uct Row22;
19643
19644 struct ˇRow44;
19645 struct Row5;
19646 struct «Rˇ»ow66;ˇ
19647
19648 «struˇ»ct Row88;
19649 struct Row9;
19650 struct Row1011;ˇ"#},
19651 vec![
19652 DiffHunkStatusKind::Modified,
19653 DiffHunkStatusKind::Modified,
19654 DiffHunkStatusKind::Modified,
19655 DiffHunkStatusKind::Modified,
19656 DiffHunkStatusKind::Modified,
19657 DiffHunkStatusKind::Modified,
19658 ],
19659 indoc! {r#"struct Row;
19660 ˇstruct Row1;
19661 struct Row2;
19662 ˇ
19663 struct Row4;
19664 ˇstruct Row5;
19665 struct Row6;
19666 ˇ
19667 struct Row8;
19668 ˇstruct Row9;
19669 struct Row10;ˇ"#},
19670 base_text,
19671 &mut cx,
19672 );
19673}
19674
19675#[gpui::test]
19676async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
19677 init_test(cx, |_| {});
19678 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19679 let base_text = indoc! {r#"
19680 one
19681
19682 two
19683 three
19684 "#};
19685
19686 cx.set_head_text(base_text);
19687 cx.set_state("\nˇ\n");
19688 cx.executor().run_until_parked();
19689 cx.update_editor(|editor, _window, cx| {
19690 editor.expand_selected_diff_hunks(cx);
19691 });
19692 cx.executor().run_until_parked();
19693 cx.update_editor(|editor, window, cx| {
19694 editor.backspace(&Default::default(), window, cx);
19695 });
19696 cx.run_until_parked();
19697 cx.assert_state_with_diff(
19698 indoc! {r#"
19699
19700 - two
19701 - threeˇ
19702 +
19703 "#}
19704 .to_string(),
19705 );
19706}
19707
19708#[gpui::test]
19709async fn test_deletion_reverts(cx: &mut TestAppContext) {
19710 init_test(cx, |_| {});
19711 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19712 let base_text = indoc! {r#"struct Row;
19713struct Row1;
19714struct Row2;
19715
19716struct Row4;
19717struct Row5;
19718struct Row6;
19719
19720struct Row8;
19721struct Row9;
19722struct Row10;"#};
19723
19724 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
19725 assert_hunk_revert(
19726 indoc! {r#"struct Row;
19727 struct Row2;
19728
19729 ˇstruct Row4;
19730 struct Row5;
19731 struct Row6;
19732 ˇ
19733 struct Row8;
19734 struct Row10;"#},
19735 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19736 indoc! {r#"struct Row;
19737 struct Row2;
19738
19739 ˇstruct Row4;
19740 struct Row5;
19741 struct Row6;
19742 ˇ
19743 struct Row8;
19744 struct Row10;"#},
19745 base_text,
19746 &mut cx,
19747 );
19748 assert_hunk_revert(
19749 indoc! {r#"struct Row;
19750 struct Row2;
19751
19752 «ˇstruct Row4;
19753 struct» Row5;
19754 «struct Row6;
19755 ˇ»
19756 struct Row8;
19757 struct Row10;"#},
19758 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19759 indoc! {r#"struct Row;
19760 struct Row2;
19761
19762 «ˇstruct Row4;
19763 struct» Row5;
19764 «struct Row6;
19765 ˇ»
19766 struct Row8;
19767 struct Row10;"#},
19768 base_text,
19769 &mut cx,
19770 );
19771
19772 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
19773 assert_hunk_revert(
19774 indoc! {r#"struct Row;
19775 ˇstruct Row2;
19776
19777 struct Row4;
19778 struct Row5;
19779 struct Row6;
19780
19781 struct Row8;ˇ
19782 struct Row10;"#},
19783 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
19784 indoc! {r#"struct Row;
19785 struct Row1;
19786 ˇstruct Row2;
19787
19788 struct Row4;
19789 struct Row5;
19790 struct Row6;
19791
19792 struct Row8;ˇ
19793 struct Row9;
19794 struct Row10;"#},
19795 base_text,
19796 &mut cx,
19797 );
19798 assert_hunk_revert(
19799 indoc! {r#"struct Row;
19800 struct Row2«ˇ;
19801 struct Row4;
19802 struct» Row5;
19803 «struct Row6;
19804
19805 struct Row8;ˇ»
19806 struct Row10;"#},
19807 vec![
19808 DiffHunkStatusKind::Deleted,
19809 DiffHunkStatusKind::Deleted,
19810 DiffHunkStatusKind::Deleted,
19811 ],
19812 indoc! {r#"struct Row;
19813 struct Row1;
19814 struct Row2«ˇ;
19815
19816 struct Row4;
19817 struct» Row5;
19818 «struct Row6;
19819
19820 struct Row8;ˇ»
19821 struct Row9;
19822 struct Row10;"#},
19823 base_text,
19824 &mut cx,
19825 );
19826}
19827
19828#[gpui::test]
19829async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
19830 init_test(cx, |_| {});
19831
19832 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
19833 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
19834 let base_text_3 =
19835 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
19836
19837 let text_1 = edit_first_char_of_every_line(base_text_1);
19838 let text_2 = edit_first_char_of_every_line(base_text_2);
19839 let text_3 = edit_first_char_of_every_line(base_text_3);
19840
19841 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
19842 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
19843 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
19844
19845 let multibuffer = cx.new(|cx| {
19846 let mut multibuffer = MultiBuffer::new(ReadWrite);
19847 multibuffer.push_excerpts(
19848 buffer_1.clone(),
19849 [
19850 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19851 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19852 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19853 ],
19854 cx,
19855 );
19856 multibuffer.push_excerpts(
19857 buffer_2.clone(),
19858 [
19859 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19860 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19861 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19862 ],
19863 cx,
19864 );
19865 multibuffer.push_excerpts(
19866 buffer_3.clone(),
19867 [
19868 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19869 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19870 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19871 ],
19872 cx,
19873 );
19874 multibuffer
19875 });
19876
19877 let fs = FakeFs::new(cx.executor());
19878 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19879 let (editor, cx) = cx
19880 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
19881 editor.update_in(cx, |editor, _window, cx| {
19882 for (buffer, diff_base) in [
19883 (buffer_1.clone(), base_text_1),
19884 (buffer_2.clone(), base_text_2),
19885 (buffer_3.clone(), base_text_3),
19886 ] {
19887 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19888 editor
19889 .buffer
19890 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19891 }
19892 });
19893 cx.executor().run_until_parked();
19894
19895 editor.update_in(cx, |editor, window, cx| {
19896 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}");
19897 editor.select_all(&SelectAll, window, cx);
19898 editor.git_restore(&Default::default(), window, cx);
19899 });
19900 cx.executor().run_until_parked();
19901
19902 // When all ranges are selected, all buffer hunks are reverted.
19903 editor.update(cx, |editor, cx| {
19904 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");
19905 });
19906 buffer_1.update(cx, |buffer, _| {
19907 assert_eq!(buffer.text(), base_text_1);
19908 });
19909 buffer_2.update(cx, |buffer, _| {
19910 assert_eq!(buffer.text(), base_text_2);
19911 });
19912 buffer_3.update(cx, |buffer, _| {
19913 assert_eq!(buffer.text(), base_text_3);
19914 });
19915
19916 editor.update_in(cx, |editor, window, cx| {
19917 editor.undo(&Default::default(), window, cx);
19918 });
19919
19920 editor.update_in(cx, |editor, window, cx| {
19921 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19922 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
19923 });
19924 editor.git_restore(&Default::default(), window, cx);
19925 });
19926
19927 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
19928 // but not affect buffer_2 and its related excerpts.
19929 editor.update(cx, |editor, cx| {
19930 assert_eq!(
19931 editor.text(cx),
19932 "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}"
19933 );
19934 });
19935 buffer_1.update(cx, |buffer, _| {
19936 assert_eq!(buffer.text(), base_text_1);
19937 });
19938 buffer_2.update(cx, |buffer, _| {
19939 assert_eq!(
19940 buffer.text(),
19941 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
19942 );
19943 });
19944 buffer_3.update(cx, |buffer, _| {
19945 assert_eq!(
19946 buffer.text(),
19947 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
19948 );
19949 });
19950
19951 fn edit_first_char_of_every_line(text: &str) -> String {
19952 text.split('\n')
19953 .map(|line| format!("X{}", &line[1..]))
19954 .collect::<Vec<_>>()
19955 .join("\n")
19956 }
19957}
19958
19959#[gpui::test]
19960async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
19961 init_test(cx, |_| {});
19962
19963 let cols = 4;
19964 let rows = 10;
19965 let sample_text_1 = sample_text(rows, cols, 'a');
19966 assert_eq!(
19967 sample_text_1,
19968 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
19969 );
19970 let sample_text_2 = sample_text(rows, cols, 'l');
19971 assert_eq!(
19972 sample_text_2,
19973 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
19974 );
19975 let sample_text_3 = sample_text(rows, cols, 'v');
19976 assert_eq!(
19977 sample_text_3,
19978 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
19979 );
19980
19981 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
19982 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
19983 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
19984
19985 let multi_buffer = cx.new(|cx| {
19986 let mut multibuffer = MultiBuffer::new(ReadWrite);
19987 multibuffer.push_excerpts(
19988 buffer_1.clone(),
19989 [
19990 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19991 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19992 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19993 ],
19994 cx,
19995 );
19996 multibuffer.push_excerpts(
19997 buffer_2.clone(),
19998 [
19999 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20000 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20001 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20002 ],
20003 cx,
20004 );
20005 multibuffer.push_excerpts(
20006 buffer_3.clone(),
20007 [
20008 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20009 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20010 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20011 ],
20012 cx,
20013 );
20014 multibuffer
20015 });
20016
20017 let fs = FakeFs::new(cx.executor());
20018 fs.insert_tree(
20019 "/a",
20020 json!({
20021 "main.rs": sample_text_1,
20022 "other.rs": sample_text_2,
20023 "lib.rs": sample_text_3,
20024 }),
20025 )
20026 .await;
20027 let project = Project::test(fs, ["/a".as_ref()], cx).await;
20028 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20029 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20030 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20031 Editor::new(
20032 EditorMode::full(),
20033 multi_buffer,
20034 Some(project.clone()),
20035 window,
20036 cx,
20037 )
20038 });
20039 let multibuffer_item_id = workspace
20040 .update(cx, |workspace, window, cx| {
20041 assert!(
20042 workspace.active_item(cx).is_none(),
20043 "active item should be None before the first item is added"
20044 );
20045 workspace.add_item_to_active_pane(
20046 Box::new(multi_buffer_editor.clone()),
20047 None,
20048 true,
20049 window,
20050 cx,
20051 );
20052 let active_item = workspace
20053 .active_item(cx)
20054 .expect("should have an active item after adding the multi buffer");
20055 assert_eq!(
20056 active_item.buffer_kind(cx),
20057 ItemBufferKind::Multibuffer,
20058 "A multi buffer was expected to active after adding"
20059 );
20060 active_item.item_id()
20061 })
20062 .unwrap();
20063 cx.executor().run_until_parked();
20064
20065 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20066 editor.change_selections(
20067 SelectionEffects::scroll(Autoscroll::Next),
20068 window,
20069 cx,
20070 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
20071 );
20072 editor.open_excerpts(&OpenExcerpts, window, cx);
20073 });
20074 cx.executor().run_until_parked();
20075 let first_item_id = workspace
20076 .update(cx, |workspace, window, cx| {
20077 let active_item = workspace
20078 .active_item(cx)
20079 .expect("should have an active item after navigating into the 1st buffer");
20080 let first_item_id = active_item.item_id();
20081 assert_ne!(
20082 first_item_id, multibuffer_item_id,
20083 "Should navigate into the 1st buffer and activate it"
20084 );
20085 assert_eq!(
20086 active_item.buffer_kind(cx),
20087 ItemBufferKind::Singleton,
20088 "New active item should be a singleton buffer"
20089 );
20090 assert_eq!(
20091 active_item
20092 .act_as::<Editor>(cx)
20093 .expect("should have navigated into an editor for the 1st buffer")
20094 .read(cx)
20095 .text(cx),
20096 sample_text_1
20097 );
20098
20099 workspace
20100 .go_back(workspace.active_pane().downgrade(), window, cx)
20101 .detach_and_log_err(cx);
20102
20103 first_item_id
20104 })
20105 .unwrap();
20106 cx.executor().run_until_parked();
20107 workspace
20108 .update(cx, |workspace, _, cx| {
20109 let active_item = workspace
20110 .active_item(cx)
20111 .expect("should have an active item after navigating back");
20112 assert_eq!(
20113 active_item.item_id(),
20114 multibuffer_item_id,
20115 "Should navigate back to the multi buffer"
20116 );
20117 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20118 })
20119 .unwrap();
20120
20121 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20122 editor.change_selections(
20123 SelectionEffects::scroll(Autoscroll::Next),
20124 window,
20125 cx,
20126 |s| s.select_ranges(Some(MultiBufferOffset(39)..MultiBufferOffset(40))),
20127 );
20128 editor.open_excerpts(&OpenExcerpts, window, cx);
20129 });
20130 cx.executor().run_until_parked();
20131 let second_item_id = workspace
20132 .update(cx, |workspace, window, cx| {
20133 let active_item = workspace
20134 .active_item(cx)
20135 .expect("should have an active item after navigating into the 2nd buffer");
20136 let second_item_id = active_item.item_id();
20137 assert_ne!(
20138 second_item_id, multibuffer_item_id,
20139 "Should navigate away from the multibuffer"
20140 );
20141 assert_ne!(
20142 second_item_id, first_item_id,
20143 "Should navigate into the 2nd buffer and activate it"
20144 );
20145 assert_eq!(
20146 active_item.buffer_kind(cx),
20147 ItemBufferKind::Singleton,
20148 "New active item should be a singleton buffer"
20149 );
20150 assert_eq!(
20151 active_item
20152 .act_as::<Editor>(cx)
20153 .expect("should have navigated into an editor")
20154 .read(cx)
20155 .text(cx),
20156 sample_text_2
20157 );
20158
20159 workspace
20160 .go_back(workspace.active_pane().downgrade(), window, cx)
20161 .detach_and_log_err(cx);
20162
20163 second_item_id
20164 })
20165 .unwrap();
20166 cx.executor().run_until_parked();
20167 workspace
20168 .update(cx, |workspace, _, cx| {
20169 let active_item = workspace
20170 .active_item(cx)
20171 .expect("should have an active item after navigating back from the 2nd buffer");
20172 assert_eq!(
20173 active_item.item_id(),
20174 multibuffer_item_id,
20175 "Should navigate back from the 2nd buffer to the multi buffer"
20176 );
20177 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20178 })
20179 .unwrap();
20180
20181 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20182 editor.change_selections(
20183 SelectionEffects::scroll(Autoscroll::Next),
20184 window,
20185 cx,
20186 |s| s.select_ranges(Some(MultiBufferOffset(70)..MultiBufferOffset(70))),
20187 );
20188 editor.open_excerpts(&OpenExcerpts, window, cx);
20189 });
20190 cx.executor().run_until_parked();
20191 workspace
20192 .update(cx, |workspace, window, cx| {
20193 let active_item = workspace
20194 .active_item(cx)
20195 .expect("should have an active item after navigating into the 3rd buffer");
20196 let third_item_id = active_item.item_id();
20197 assert_ne!(
20198 third_item_id, multibuffer_item_id,
20199 "Should navigate into the 3rd buffer and activate it"
20200 );
20201 assert_ne!(third_item_id, first_item_id);
20202 assert_ne!(third_item_id, second_item_id);
20203 assert_eq!(
20204 active_item.buffer_kind(cx),
20205 ItemBufferKind::Singleton,
20206 "New active item should be a singleton buffer"
20207 );
20208 assert_eq!(
20209 active_item
20210 .act_as::<Editor>(cx)
20211 .expect("should have navigated into an editor")
20212 .read(cx)
20213 .text(cx),
20214 sample_text_3
20215 );
20216
20217 workspace
20218 .go_back(workspace.active_pane().downgrade(), window, cx)
20219 .detach_and_log_err(cx);
20220 })
20221 .unwrap();
20222 cx.executor().run_until_parked();
20223 workspace
20224 .update(cx, |workspace, _, cx| {
20225 let active_item = workspace
20226 .active_item(cx)
20227 .expect("should have an active item after navigating back from the 3rd buffer");
20228 assert_eq!(
20229 active_item.item_id(),
20230 multibuffer_item_id,
20231 "Should navigate back from the 3rd buffer to the multi buffer"
20232 );
20233 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20234 })
20235 .unwrap();
20236}
20237
20238#[gpui::test]
20239async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20240 init_test(cx, |_| {});
20241
20242 let mut cx = EditorTestContext::new(cx).await;
20243
20244 let diff_base = r#"
20245 use some::mod;
20246
20247 const A: u32 = 42;
20248
20249 fn main() {
20250 println!("hello");
20251
20252 println!("world");
20253 }
20254 "#
20255 .unindent();
20256
20257 cx.set_state(
20258 &r#"
20259 use some::modified;
20260
20261 ˇ
20262 fn main() {
20263 println!("hello there");
20264
20265 println!("around the");
20266 println!("world");
20267 }
20268 "#
20269 .unindent(),
20270 );
20271
20272 cx.set_head_text(&diff_base);
20273 executor.run_until_parked();
20274
20275 cx.update_editor(|editor, window, cx| {
20276 editor.go_to_next_hunk(&GoToHunk, window, cx);
20277 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20278 });
20279 executor.run_until_parked();
20280 cx.assert_state_with_diff(
20281 r#"
20282 use some::modified;
20283
20284
20285 fn main() {
20286 - println!("hello");
20287 + ˇ println!("hello there");
20288
20289 println!("around the");
20290 println!("world");
20291 }
20292 "#
20293 .unindent(),
20294 );
20295
20296 cx.update_editor(|editor, window, cx| {
20297 for _ in 0..2 {
20298 editor.go_to_next_hunk(&GoToHunk, window, cx);
20299 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20300 }
20301 });
20302 executor.run_until_parked();
20303 cx.assert_state_with_diff(
20304 r#"
20305 - use some::mod;
20306 + ˇuse some::modified;
20307
20308
20309 fn main() {
20310 - println!("hello");
20311 + println!("hello there");
20312
20313 + println!("around the");
20314 println!("world");
20315 }
20316 "#
20317 .unindent(),
20318 );
20319
20320 cx.update_editor(|editor, window, cx| {
20321 editor.go_to_next_hunk(&GoToHunk, window, cx);
20322 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20323 });
20324 executor.run_until_parked();
20325 cx.assert_state_with_diff(
20326 r#"
20327 - use some::mod;
20328 + use some::modified;
20329
20330 - const A: u32 = 42;
20331 ˇ
20332 fn main() {
20333 - println!("hello");
20334 + println!("hello there");
20335
20336 + println!("around the");
20337 println!("world");
20338 }
20339 "#
20340 .unindent(),
20341 );
20342
20343 cx.update_editor(|editor, window, cx| {
20344 editor.cancel(&Cancel, window, cx);
20345 });
20346
20347 cx.assert_state_with_diff(
20348 r#"
20349 use some::modified;
20350
20351 ˇ
20352 fn main() {
20353 println!("hello there");
20354
20355 println!("around the");
20356 println!("world");
20357 }
20358 "#
20359 .unindent(),
20360 );
20361}
20362
20363#[gpui::test]
20364async fn test_diff_base_change_with_expanded_diff_hunks(
20365 executor: BackgroundExecutor,
20366 cx: &mut TestAppContext,
20367) {
20368 init_test(cx, |_| {});
20369
20370 let mut cx = EditorTestContext::new(cx).await;
20371
20372 let diff_base = r#"
20373 use some::mod1;
20374 use some::mod2;
20375
20376 const A: u32 = 42;
20377 const B: u32 = 42;
20378 const C: u32 = 42;
20379
20380 fn main() {
20381 println!("hello");
20382
20383 println!("world");
20384 }
20385 "#
20386 .unindent();
20387
20388 cx.set_state(
20389 &r#"
20390 use some::mod2;
20391
20392 const A: u32 = 42;
20393 const C: u32 = 42;
20394
20395 fn main(ˇ) {
20396 //println!("hello");
20397
20398 println!("world");
20399 //
20400 //
20401 }
20402 "#
20403 .unindent(),
20404 );
20405
20406 cx.set_head_text(&diff_base);
20407 executor.run_until_parked();
20408
20409 cx.update_editor(|editor, window, cx| {
20410 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20411 });
20412 executor.run_until_parked();
20413 cx.assert_state_with_diff(
20414 r#"
20415 - use some::mod1;
20416 use some::mod2;
20417
20418 const A: u32 = 42;
20419 - const B: u32 = 42;
20420 const C: u32 = 42;
20421
20422 fn main(ˇ) {
20423 - println!("hello");
20424 + //println!("hello");
20425
20426 println!("world");
20427 + //
20428 + //
20429 }
20430 "#
20431 .unindent(),
20432 );
20433
20434 cx.set_head_text("new diff base!");
20435 executor.run_until_parked();
20436 cx.assert_state_with_diff(
20437 r#"
20438 - new diff base!
20439 + use some::mod2;
20440 +
20441 + const A: u32 = 42;
20442 + const C: u32 = 42;
20443 +
20444 + fn main(ˇ) {
20445 + //println!("hello");
20446 +
20447 + println!("world");
20448 + //
20449 + //
20450 + }
20451 "#
20452 .unindent(),
20453 );
20454}
20455
20456#[gpui::test]
20457async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
20458 init_test(cx, |_| {});
20459
20460 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20461 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20462 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20463 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20464 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
20465 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
20466
20467 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
20468 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
20469 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
20470
20471 let multi_buffer = cx.new(|cx| {
20472 let mut multibuffer = MultiBuffer::new(ReadWrite);
20473 multibuffer.push_excerpts(
20474 buffer_1.clone(),
20475 [
20476 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20477 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20478 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20479 ],
20480 cx,
20481 );
20482 multibuffer.push_excerpts(
20483 buffer_2.clone(),
20484 [
20485 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20486 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20487 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20488 ],
20489 cx,
20490 );
20491 multibuffer.push_excerpts(
20492 buffer_3.clone(),
20493 [
20494 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20495 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20496 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20497 ],
20498 cx,
20499 );
20500 multibuffer
20501 });
20502
20503 let editor =
20504 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20505 editor
20506 .update(cx, |editor, _window, cx| {
20507 for (buffer, diff_base) in [
20508 (buffer_1.clone(), file_1_old),
20509 (buffer_2.clone(), file_2_old),
20510 (buffer_3.clone(), file_3_old),
20511 ] {
20512 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
20513 editor
20514 .buffer
20515 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
20516 }
20517 })
20518 .unwrap();
20519
20520 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20521 cx.run_until_parked();
20522
20523 cx.assert_editor_state(
20524 &"
20525 ˇaaa
20526 ccc
20527 ddd
20528
20529 ggg
20530 hhh
20531
20532
20533 lll
20534 mmm
20535 NNN
20536
20537 qqq
20538 rrr
20539
20540 uuu
20541 111
20542 222
20543 333
20544
20545 666
20546 777
20547
20548 000
20549 !!!"
20550 .unindent(),
20551 );
20552
20553 cx.update_editor(|editor, window, cx| {
20554 editor.select_all(&SelectAll, window, cx);
20555 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20556 });
20557 cx.executor().run_until_parked();
20558
20559 cx.assert_state_with_diff(
20560 "
20561 «aaa
20562 - bbb
20563 ccc
20564 ddd
20565
20566 ggg
20567 hhh
20568
20569
20570 lll
20571 mmm
20572 - nnn
20573 + NNN
20574
20575 qqq
20576 rrr
20577
20578 uuu
20579 111
20580 222
20581 333
20582
20583 + 666
20584 777
20585
20586 000
20587 !!!ˇ»"
20588 .unindent(),
20589 );
20590}
20591
20592#[gpui::test]
20593async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
20594 init_test(cx, |_| {});
20595
20596 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
20597 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
20598
20599 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
20600 let multi_buffer = cx.new(|cx| {
20601 let mut multibuffer = MultiBuffer::new(ReadWrite);
20602 multibuffer.push_excerpts(
20603 buffer.clone(),
20604 [
20605 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
20606 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
20607 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
20608 ],
20609 cx,
20610 );
20611 multibuffer
20612 });
20613
20614 let editor =
20615 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20616 editor
20617 .update(cx, |editor, _window, cx| {
20618 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
20619 editor
20620 .buffer
20621 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
20622 })
20623 .unwrap();
20624
20625 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20626 cx.run_until_parked();
20627
20628 cx.update_editor(|editor, window, cx| {
20629 editor.expand_all_diff_hunks(&Default::default(), window, cx)
20630 });
20631 cx.executor().run_until_parked();
20632
20633 // When the start of a hunk coincides with the start of its excerpt,
20634 // the hunk is expanded. When the start of a hunk is earlier than
20635 // the start of its excerpt, the hunk is not expanded.
20636 cx.assert_state_with_diff(
20637 "
20638 ˇaaa
20639 - bbb
20640 + BBB
20641
20642 - ddd
20643 - eee
20644 + DDD
20645 + EEE
20646 fff
20647
20648 iii
20649 "
20650 .unindent(),
20651 );
20652}
20653
20654#[gpui::test]
20655async fn test_edits_around_expanded_insertion_hunks(
20656 executor: BackgroundExecutor,
20657 cx: &mut TestAppContext,
20658) {
20659 init_test(cx, |_| {});
20660
20661 let mut cx = EditorTestContext::new(cx).await;
20662
20663 let diff_base = r#"
20664 use some::mod1;
20665 use some::mod2;
20666
20667 const A: u32 = 42;
20668
20669 fn main() {
20670 println!("hello");
20671
20672 println!("world");
20673 }
20674 "#
20675 .unindent();
20676 executor.run_until_parked();
20677 cx.set_state(
20678 &r#"
20679 use some::mod1;
20680 use some::mod2;
20681
20682 const A: u32 = 42;
20683 const B: u32 = 42;
20684 const C: u32 = 42;
20685 ˇ
20686
20687 fn main() {
20688 println!("hello");
20689
20690 println!("world");
20691 }
20692 "#
20693 .unindent(),
20694 );
20695
20696 cx.set_head_text(&diff_base);
20697 executor.run_until_parked();
20698
20699 cx.update_editor(|editor, window, cx| {
20700 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20701 });
20702 executor.run_until_parked();
20703
20704 cx.assert_state_with_diff(
20705 r#"
20706 use some::mod1;
20707 use some::mod2;
20708
20709 const A: u32 = 42;
20710 + const B: u32 = 42;
20711 + const C: u32 = 42;
20712 + ˇ
20713
20714 fn main() {
20715 println!("hello");
20716
20717 println!("world");
20718 }
20719 "#
20720 .unindent(),
20721 );
20722
20723 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
20724 executor.run_until_parked();
20725
20726 cx.assert_state_with_diff(
20727 r#"
20728 use some::mod1;
20729 use some::mod2;
20730
20731 const A: u32 = 42;
20732 + const B: u32 = 42;
20733 + const C: u32 = 42;
20734 + const D: u32 = 42;
20735 + ˇ
20736
20737 fn main() {
20738 println!("hello");
20739
20740 println!("world");
20741 }
20742 "#
20743 .unindent(),
20744 );
20745
20746 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
20747 executor.run_until_parked();
20748
20749 cx.assert_state_with_diff(
20750 r#"
20751 use some::mod1;
20752 use some::mod2;
20753
20754 const A: u32 = 42;
20755 + const B: u32 = 42;
20756 + const C: u32 = 42;
20757 + const D: u32 = 42;
20758 + const E: u32 = 42;
20759 + ˇ
20760
20761 fn main() {
20762 println!("hello");
20763
20764 println!("world");
20765 }
20766 "#
20767 .unindent(),
20768 );
20769
20770 cx.update_editor(|editor, window, cx| {
20771 editor.delete_line(&DeleteLine, window, cx);
20772 });
20773 executor.run_until_parked();
20774
20775 cx.assert_state_with_diff(
20776 r#"
20777 use some::mod1;
20778 use some::mod2;
20779
20780 const A: u32 = 42;
20781 + const B: u32 = 42;
20782 + const C: u32 = 42;
20783 + const D: u32 = 42;
20784 + const E: u32 = 42;
20785 ˇ
20786 fn main() {
20787 println!("hello");
20788
20789 println!("world");
20790 }
20791 "#
20792 .unindent(),
20793 );
20794
20795 cx.update_editor(|editor, window, cx| {
20796 editor.move_up(&MoveUp, window, cx);
20797 editor.delete_line(&DeleteLine, window, cx);
20798 editor.move_up(&MoveUp, window, cx);
20799 editor.delete_line(&DeleteLine, window, cx);
20800 editor.move_up(&MoveUp, window, cx);
20801 editor.delete_line(&DeleteLine, window, cx);
20802 });
20803 executor.run_until_parked();
20804 cx.assert_state_with_diff(
20805 r#"
20806 use some::mod1;
20807 use some::mod2;
20808
20809 const A: u32 = 42;
20810 + const B: u32 = 42;
20811 ˇ
20812 fn main() {
20813 println!("hello");
20814
20815 println!("world");
20816 }
20817 "#
20818 .unindent(),
20819 );
20820
20821 cx.update_editor(|editor, window, cx| {
20822 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
20823 editor.delete_line(&DeleteLine, window, cx);
20824 });
20825 executor.run_until_parked();
20826 cx.assert_state_with_diff(
20827 r#"
20828 ˇ
20829 fn main() {
20830 println!("hello");
20831
20832 println!("world");
20833 }
20834 "#
20835 .unindent(),
20836 );
20837}
20838
20839#[gpui::test]
20840async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
20841 init_test(cx, |_| {});
20842
20843 let mut cx = EditorTestContext::new(cx).await;
20844 cx.set_head_text(indoc! { "
20845 one
20846 two
20847 three
20848 four
20849 five
20850 "
20851 });
20852 cx.set_state(indoc! { "
20853 one
20854 ˇthree
20855 five
20856 "});
20857 cx.run_until_parked();
20858 cx.update_editor(|editor, window, cx| {
20859 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20860 });
20861 cx.assert_state_with_diff(
20862 indoc! { "
20863 one
20864 - two
20865 ˇthree
20866 - four
20867 five
20868 "}
20869 .to_string(),
20870 );
20871 cx.update_editor(|editor, window, cx| {
20872 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20873 });
20874
20875 cx.assert_state_with_diff(
20876 indoc! { "
20877 one
20878 ˇthree
20879 five
20880 "}
20881 .to_string(),
20882 );
20883
20884 cx.update_editor(|editor, window, cx| {
20885 editor.move_up(&MoveUp, window, cx);
20886 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20887 });
20888 cx.assert_state_with_diff(
20889 indoc! { "
20890 ˇone
20891 - two
20892 three
20893 five
20894 "}
20895 .to_string(),
20896 );
20897
20898 cx.update_editor(|editor, window, cx| {
20899 editor.move_down(&MoveDown, window, cx);
20900 editor.move_down(&MoveDown, window, cx);
20901 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20902 });
20903 cx.assert_state_with_diff(
20904 indoc! { "
20905 one
20906 - two
20907 ˇthree
20908 - four
20909 five
20910 "}
20911 .to_string(),
20912 );
20913
20914 cx.set_state(indoc! { "
20915 one
20916 ˇTWO
20917 three
20918 four
20919 five
20920 "});
20921 cx.run_until_parked();
20922 cx.update_editor(|editor, window, cx| {
20923 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20924 });
20925
20926 cx.assert_state_with_diff(
20927 indoc! { "
20928 one
20929 - two
20930 + ˇTWO
20931 three
20932 four
20933 five
20934 "}
20935 .to_string(),
20936 );
20937 cx.update_editor(|editor, window, cx| {
20938 editor.move_up(&Default::default(), window, cx);
20939 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20940 });
20941 cx.assert_state_with_diff(
20942 indoc! { "
20943 one
20944 ˇTWO
20945 three
20946 four
20947 five
20948 "}
20949 .to_string(),
20950 );
20951}
20952
20953#[gpui::test]
20954async fn test_toggling_adjacent_diff_hunks_2(
20955 executor: BackgroundExecutor,
20956 cx: &mut TestAppContext,
20957) {
20958 init_test(cx, |_| {});
20959
20960 let mut cx = EditorTestContext::new(cx).await;
20961
20962 let diff_base = r#"
20963 lineA
20964 lineB
20965 lineC
20966 lineD
20967 "#
20968 .unindent();
20969
20970 cx.set_state(
20971 &r#"
20972 ˇlineA1
20973 lineB
20974 lineD
20975 "#
20976 .unindent(),
20977 );
20978 cx.set_head_text(&diff_base);
20979 executor.run_until_parked();
20980
20981 cx.update_editor(|editor, window, cx| {
20982 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20983 });
20984 executor.run_until_parked();
20985 cx.assert_state_with_diff(
20986 r#"
20987 - lineA
20988 + ˇlineA1
20989 lineB
20990 lineD
20991 "#
20992 .unindent(),
20993 );
20994
20995 cx.update_editor(|editor, window, cx| {
20996 editor.move_down(&MoveDown, window, cx);
20997 editor.move_right(&MoveRight, window, cx);
20998 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20999 });
21000 executor.run_until_parked();
21001 cx.assert_state_with_diff(
21002 r#"
21003 - lineA
21004 + lineA1
21005 lˇineB
21006 - lineC
21007 lineD
21008 "#
21009 .unindent(),
21010 );
21011}
21012
21013#[gpui::test]
21014async fn test_edits_around_expanded_deletion_hunks(
21015 executor: BackgroundExecutor,
21016 cx: &mut TestAppContext,
21017) {
21018 init_test(cx, |_| {});
21019
21020 let mut cx = EditorTestContext::new(cx).await;
21021
21022 let diff_base = r#"
21023 use some::mod1;
21024 use some::mod2;
21025
21026 const A: u32 = 42;
21027 const B: u32 = 42;
21028 const C: u32 = 42;
21029
21030
21031 fn main() {
21032 println!("hello");
21033
21034 println!("world");
21035 }
21036 "#
21037 .unindent();
21038 executor.run_until_parked();
21039 cx.set_state(
21040 &r#"
21041 use some::mod1;
21042 use some::mod2;
21043
21044 ˇconst B: u32 = 42;
21045 const C: u32 = 42;
21046
21047
21048 fn main() {
21049 println!("hello");
21050
21051 println!("world");
21052 }
21053 "#
21054 .unindent(),
21055 );
21056
21057 cx.set_head_text(&diff_base);
21058 executor.run_until_parked();
21059
21060 cx.update_editor(|editor, window, cx| {
21061 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21062 });
21063 executor.run_until_parked();
21064
21065 cx.assert_state_with_diff(
21066 r#"
21067 use some::mod1;
21068 use some::mod2;
21069
21070 - const A: u32 = 42;
21071 ˇconst B: u32 = 42;
21072 const C: u32 = 42;
21073
21074
21075 fn main() {
21076 println!("hello");
21077
21078 println!("world");
21079 }
21080 "#
21081 .unindent(),
21082 );
21083
21084 cx.update_editor(|editor, window, cx| {
21085 editor.delete_line(&DeleteLine, window, cx);
21086 });
21087 executor.run_until_parked();
21088 cx.assert_state_with_diff(
21089 r#"
21090 use some::mod1;
21091 use some::mod2;
21092
21093 - const A: u32 = 42;
21094 - const B: u32 = 42;
21095 ˇconst C: u32 = 42;
21096
21097
21098 fn main() {
21099 println!("hello");
21100
21101 println!("world");
21102 }
21103 "#
21104 .unindent(),
21105 );
21106
21107 cx.update_editor(|editor, window, cx| {
21108 editor.delete_line(&DeleteLine, window, cx);
21109 });
21110 executor.run_until_parked();
21111 cx.assert_state_with_diff(
21112 r#"
21113 use some::mod1;
21114 use some::mod2;
21115
21116 - const A: u32 = 42;
21117 - const B: u32 = 42;
21118 - const C: u32 = 42;
21119 ˇ
21120
21121 fn main() {
21122 println!("hello");
21123
21124 println!("world");
21125 }
21126 "#
21127 .unindent(),
21128 );
21129
21130 cx.update_editor(|editor, window, cx| {
21131 editor.handle_input("replacement", window, cx);
21132 });
21133 executor.run_until_parked();
21134 cx.assert_state_with_diff(
21135 r#"
21136 use some::mod1;
21137 use some::mod2;
21138
21139 - const A: u32 = 42;
21140 - const B: u32 = 42;
21141 - const C: u32 = 42;
21142 -
21143 + replacementˇ
21144
21145 fn main() {
21146 println!("hello");
21147
21148 println!("world");
21149 }
21150 "#
21151 .unindent(),
21152 );
21153}
21154
21155#[gpui::test]
21156async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21157 init_test(cx, |_| {});
21158
21159 let mut cx = EditorTestContext::new(cx).await;
21160
21161 let base_text = r#"
21162 one
21163 two
21164 three
21165 four
21166 five
21167 "#
21168 .unindent();
21169 executor.run_until_parked();
21170 cx.set_state(
21171 &r#"
21172 one
21173 two
21174 fˇour
21175 five
21176 "#
21177 .unindent(),
21178 );
21179
21180 cx.set_head_text(&base_text);
21181 executor.run_until_parked();
21182
21183 cx.update_editor(|editor, window, cx| {
21184 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21185 });
21186 executor.run_until_parked();
21187
21188 cx.assert_state_with_diff(
21189 r#"
21190 one
21191 two
21192 - three
21193 fˇour
21194 five
21195 "#
21196 .unindent(),
21197 );
21198
21199 cx.update_editor(|editor, window, cx| {
21200 editor.backspace(&Backspace, window, cx);
21201 editor.backspace(&Backspace, window, cx);
21202 });
21203 executor.run_until_parked();
21204 cx.assert_state_with_diff(
21205 r#"
21206 one
21207 two
21208 - threeˇ
21209 - four
21210 + our
21211 five
21212 "#
21213 .unindent(),
21214 );
21215}
21216
21217#[gpui::test]
21218async fn test_edit_after_expanded_modification_hunk(
21219 executor: BackgroundExecutor,
21220 cx: &mut TestAppContext,
21221) {
21222 init_test(cx, |_| {});
21223
21224 let mut cx = EditorTestContext::new(cx).await;
21225
21226 let diff_base = r#"
21227 use some::mod1;
21228 use some::mod2;
21229
21230 const A: u32 = 42;
21231 const B: u32 = 42;
21232 const C: u32 = 42;
21233 const D: u32 = 42;
21234
21235
21236 fn main() {
21237 println!("hello");
21238
21239 println!("world");
21240 }"#
21241 .unindent();
21242
21243 cx.set_state(
21244 &r#"
21245 use some::mod1;
21246 use some::mod2;
21247
21248 const A: u32 = 42;
21249 const B: u32 = 42;
21250 const C: u32 = 43ˇ
21251 const D: u32 = 42;
21252
21253
21254 fn main() {
21255 println!("hello");
21256
21257 println!("world");
21258 }"#
21259 .unindent(),
21260 );
21261
21262 cx.set_head_text(&diff_base);
21263 executor.run_until_parked();
21264 cx.update_editor(|editor, window, cx| {
21265 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21266 });
21267 executor.run_until_parked();
21268
21269 cx.assert_state_with_diff(
21270 r#"
21271 use some::mod1;
21272 use some::mod2;
21273
21274 const A: u32 = 42;
21275 const B: u32 = 42;
21276 - const C: u32 = 42;
21277 + const C: u32 = 43ˇ
21278 const D: u32 = 42;
21279
21280
21281 fn main() {
21282 println!("hello");
21283
21284 println!("world");
21285 }"#
21286 .unindent(),
21287 );
21288
21289 cx.update_editor(|editor, window, cx| {
21290 editor.handle_input("\nnew_line\n", window, cx);
21291 });
21292 executor.run_until_parked();
21293
21294 cx.assert_state_with_diff(
21295 r#"
21296 use some::mod1;
21297 use some::mod2;
21298
21299 const A: u32 = 42;
21300 const B: u32 = 42;
21301 - const C: u32 = 42;
21302 + const C: u32 = 43
21303 + new_line
21304 + ˇ
21305 const D: u32 = 42;
21306
21307
21308 fn main() {
21309 println!("hello");
21310
21311 println!("world");
21312 }"#
21313 .unindent(),
21314 );
21315}
21316
21317#[gpui::test]
21318async fn test_stage_and_unstage_added_file_hunk(
21319 executor: BackgroundExecutor,
21320 cx: &mut TestAppContext,
21321) {
21322 init_test(cx, |_| {});
21323
21324 let mut cx = EditorTestContext::new(cx).await;
21325 cx.update_editor(|editor, _, cx| {
21326 editor.set_expand_all_diff_hunks(cx);
21327 });
21328
21329 let working_copy = r#"
21330 ˇfn main() {
21331 println!("hello, world!");
21332 }
21333 "#
21334 .unindent();
21335
21336 cx.set_state(&working_copy);
21337 executor.run_until_parked();
21338
21339 cx.assert_state_with_diff(
21340 r#"
21341 + ˇfn main() {
21342 + println!("hello, world!");
21343 + }
21344 "#
21345 .unindent(),
21346 );
21347 cx.assert_index_text(None);
21348
21349 cx.update_editor(|editor, window, cx| {
21350 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21351 });
21352 executor.run_until_parked();
21353 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
21354 cx.assert_state_with_diff(
21355 r#"
21356 + ˇfn main() {
21357 + println!("hello, world!");
21358 + }
21359 "#
21360 .unindent(),
21361 );
21362
21363 cx.update_editor(|editor, window, cx| {
21364 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21365 });
21366 executor.run_until_parked();
21367 cx.assert_index_text(None);
21368}
21369
21370async fn setup_indent_guides_editor(
21371 text: &str,
21372 cx: &mut TestAppContext,
21373) -> (BufferId, EditorTestContext) {
21374 init_test(cx, |_| {});
21375
21376 let mut cx = EditorTestContext::new(cx).await;
21377
21378 let buffer_id = cx.update_editor(|editor, window, cx| {
21379 editor.set_text(text, window, cx);
21380 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
21381
21382 buffer_ids[0]
21383 });
21384
21385 (buffer_id, cx)
21386}
21387
21388fn assert_indent_guides(
21389 range: Range<u32>,
21390 expected: Vec<IndentGuide>,
21391 active_indices: Option<Vec<usize>>,
21392 cx: &mut EditorTestContext,
21393) {
21394 let indent_guides = cx.update_editor(|editor, window, cx| {
21395 let snapshot = editor.snapshot(window, cx).display_snapshot;
21396 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
21397 editor,
21398 MultiBufferRow(range.start)..MultiBufferRow(range.end),
21399 true,
21400 &snapshot,
21401 cx,
21402 );
21403
21404 indent_guides.sort_by(|a, b| {
21405 a.depth.cmp(&b.depth).then(
21406 a.start_row
21407 .cmp(&b.start_row)
21408 .then(a.end_row.cmp(&b.end_row)),
21409 )
21410 });
21411 indent_guides
21412 });
21413
21414 if let Some(expected) = active_indices {
21415 let active_indices = cx.update_editor(|editor, window, cx| {
21416 let snapshot = editor.snapshot(window, cx).display_snapshot;
21417 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
21418 });
21419
21420 assert_eq!(
21421 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
21422 expected,
21423 "Active indent guide indices do not match"
21424 );
21425 }
21426
21427 assert_eq!(indent_guides, expected, "Indent guides do not match");
21428}
21429
21430fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
21431 IndentGuide {
21432 buffer_id,
21433 start_row: MultiBufferRow(start_row),
21434 end_row: MultiBufferRow(end_row),
21435 depth,
21436 tab_size: 4,
21437 settings: IndentGuideSettings {
21438 enabled: true,
21439 line_width: 1,
21440 active_line_width: 1,
21441 coloring: IndentGuideColoring::default(),
21442 background_coloring: IndentGuideBackgroundColoring::default(),
21443 },
21444 }
21445}
21446
21447#[gpui::test]
21448async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
21449 let (buffer_id, mut cx) = setup_indent_guides_editor(
21450 &"
21451 fn main() {
21452 let a = 1;
21453 }"
21454 .unindent(),
21455 cx,
21456 )
21457 .await;
21458
21459 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21460}
21461
21462#[gpui::test]
21463async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
21464 let (buffer_id, mut cx) = setup_indent_guides_editor(
21465 &"
21466 fn main() {
21467 let a = 1;
21468 let b = 2;
21469 }"
21470 .unindent(),
21471 cx,
21472 )
21473 .await;
21474
21475 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
21476}
21477
21478#[gpui::test]
21479async fn test_indent_guide_nested(cx: &mut TestAppContext) {
21480 let (buffer_id, mut cx) = setup_indent_guides_editor(
21481 &"
21482 fn main() {
21483 let a = 1;
21484 if a == 3 {
21485 let b = 2;
21486 } else {
21487 let c = 3;
21488 }
21489 }"
21490 .unindent(),
21491 cx,
21492 )
21493 .await;
21494
21495 assert_indent_guides(
21496 0..8,
21497 vec![
21498 indent_guide(buffer_id, 1, 6, 0),
21499 indent_guide(buffer_id, 3, 3, 1),
21500 indent_guide(buffer_id, 5, 5, 1),
21501 ],
21502 None,
21503 &mut cx,
21504 );
21505}
21506
21507#[gpui::test]
21508async fn test_indent_guide_tab(cx: &mut TestAppContext) {
21509 let (buffer_id, mut cx) = setup_indent_guides_editor(
21510 &"
21511 fn main() {
21512 let a = 1;
21513 let b = 2;
21514 let c = 3;
21515 }"
21516 .unindent(),
21517 cx,
21518 )
21519 .await;
21520
21521 assert_indent_guides(
21522 0..5,
21523 vec![
21524 indent_guide(buffer_id, 1, 3, 0),
21525 indent_guide(buffer_id, 2, 2, 1),
21526 ],
21527 None,
21528 &mut cx,
21529 );
21530}
21531
21532#[gpui::test]
21533async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
21534 let (buffer_id, mut cx) = setup_indent_guides_editor(
21535 &"
21536 fn main() {
21537 let a = 1;
21538
21539 let c = 3;
21540 }"
21541 .unindent(),
21542 cx,
21543 )
21544 .await;
21545
21546 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
21547}
21548
21549#[gpui::test]
21550async fn test_indent_guide_complex(cx: &mut TestAppContext) {
21551 let (buffer_id, mut cx) = setup_indent_guides_editor(
21552 &"
21553 fn main() {
21554 let a = 1;
21555
21556 let c = 3;
21557
21558 if a == 3 {
21559 let b = 2;
21560 } else {
21561 let c = 3;
21562 }
21563 }"
21564 .unindent(),
21565 cx,
21566 )
21567 .await;
21568
21569 assert_indent_guides(
21570 0..11,
21571 vec![
21572 indent_guide(buffer_id, 1, 9, 0),
21573 indent_guide(buffer_id, 6, 6, 1),
21574 indent_guide(buffer_id, 8, 8, 1),
21575 ],
21576 None,
21577 &mut cx,
21578 );
21579}
21580
21581#[gpui::test]
21582async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
21583 let (buffer_id, mut cx) = setup_indent_guides_editor(
21584 &"
21585 fn main() {
21586 let a = 1;
21587
21588 let c = 3;
21589
21590 if a == 3 {
21591 let b = 2;
21592 } else {
21593 let c = 3;
21594 }
21595 }"
21596 .unindent(),
21597 cx,
21598 )
21599 .await;
21600
21601 assert_indent_guides(
21602 1..11,
21603 vec![
21604 indent_guide(buffer_id, 1, 9, 0),
21605 indent_guide(buffer_id, 6, 6, 1),
21606 indent_guide(buffer_id, 8, 8, 1),
21607 ],
21608 None,
21609 &mut cx,
21610 );
21611}
21612
21613#[gpui::test]
21614async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
21615 let (buffer_id, mut cx) = setup_indent_guides_editor(
21616 &"
21617 fn main() {
21618 let a = 1;
21619
21620 let c = 3;
21621
21622 if a == 3 {
21623 let b = 2;
21624 } else {
21625 let c = 3;
21626 }
21627 }"
21628 .unindent(),
21629 cx,
21630 )
21631 .await;
21632
21633 assert_indent_guides(
21634 1..10,
21635 vec![
21636 indent_guide(buffer_id, 1, 9, 0),
21637 indent_guide(buffer_id, 6, 6, 1),
21638 indent_guide(buffer_id, 8, 8, 1),
21639 ],
21640 None,
21641 &mut cx,
21642 );
21643}
21644
21645#[gpui::test]
21646async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
21647 let (buffer_id, mut cx) = setup_indent_guides_editor(
21648 &"
21649 fn main() {
21650 if a {
21651 b(
21652 c,
21653 d,
21654 )
21655 } else {
21656 e(
21657 f
21658 )
21659 }
21660 }"
21661 .unindent(),
21662 cx,
21663 )
21664 .await;
21665
21666 assert_indent_guides(
21667 0..11,
21668 vec![
21669 indent_guide(buffer_id, 1, 10, 0),
21670 indent_guide(buffer_id, 2, 5, 1),
21671 indent_guide(buffer_id, 7, 9, 1),
21672 indent_guide(buffer_id, 3, 4, 2),
21673 indent_guide(buffer_id, 8, 8, 2),
21674 ],
21675 None,
21676 &mut cx,
21677 );
21678
21679 cx.update_editor(|editor, window, cx| {
21680 editor.fold_at(MultiBufferRow(2), window, cx);
21681 assert_eq!(
21682 editor.display_text(cx),
21683 "
21684 fn main() {
21685 if a {
21686 b(⋯
21687 )
21688 } else {
21689 e(
21690 f
21691 )
21692 }
21693 }"
21694 .unindent()
21695 );
21696 });
21697
21698 assert_indent_guides(
21699 0..11,
21700 vec![
21701 indent_guide(buffer_id, 1, 10, 0),
21702 indent_guide(buffer_id, 2, 5, 1),
21703 indent_guide(buffer_id, 7, 9, 1),
21704 indent_guide(buffer_id, 8, 8, 2),
21705 ],
21706 None,
21707 &mut cx,
21708 );
21709}
21710
21711#[gpui::test]
21712async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
21713 let (buffer_id, mut cx) = setup_indent_guides_editor(
21714 &"
21715 block1
21716 block2
21717 block3
21718 block4
21719 block2
21720 block1
21721 block1"
21722 .unindent(),
21723 cx,
21724 )
21725 .await;
21726
21727 assert_indent_guides(
21728 1..10,
21729 vec![
21730 indent_guide(buffer_id, 1, 4, 0),
21731 indent_guide(buffer_id, 2, 3, 1),
21732 indent_guide(buffer_id, 3, 3, 2),
21733 ],
21734 None,
21735 &mut cx,
21736 );
21737}
21738
21739#[gpui::test]
21740async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
21741 let (buffer_id, mut cx) = setup_indent_guides_editor(
21742 &"
21743 block1
21744 block2
21745 block3
21746
21747 block1
21748 block1"
21749 .unindent(),
21750 cx,
21751 )
21752 .await;
21753
21754 assert_indent_guides(
21755 0..6,
21756 vec![
21757 indent_guide(buffer_id, 1, 2, 0),
21758 indent_guide(buffer_id, 2, 2, 1),
21759 ],
21760 None,
21761 &mut cx,
21762 );
21763}
21764
21765#[gpui::test]
21766async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
21767 let (buffer_id, mut cx) = setup_indent_guides_editor(
21768 &"
21769 function component() {
21770 \treturn (
21771 \t\t\t
21772 \t\t<div>
21773 \t\t\t<abc></abc>
21774 \t\t</div>
21775 \t)
21776 }"
21777 .unindent(),
21778 cx,
21779 )
21780 .await;
21781
21782 assert_indent_guides(
21783 0..8,
21784 vec![
21785 indent_guide(buffer_id, 1, 6, 0),
21786 indent_guide(buffer_id, 2, 5, 1),
21787 indent_guide(buffer_id, 4, 4, 2),
21788 ],
21789 None,
21790 &mut cx,
21791 );
21792}
21793
21794#[gpui::test]
21795async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
21796 let (buffer_id, mut cx) = setup_indent_guides_editor(
21797 &"
21798 function component() {
21799 \treturn (
21800 \t
21801 \t\t<div>
21802 \t\t\t<abc></abc>
21803 \t\t</div>
21804 \t)
21805 }"
21806 .unindent(),
21807 cx,
21808 )
21809 .await;
21810
21811 assert_indent_guides(
21812 0..8,
21813 vec![
21814 indent_guide(buffer_id, 1, 6, 0),
21815 indent_guide(buffer_id, 2, 5, 1),
21816 indent_guide(buffer_id, 4, 4, 2),
21817 ],
21818 None,
21819 &mut cx,
21820 );
21821}
21822
21823#[gpui::test]
21824async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
21825 let (buffer_id, mut cx) = setup_indent_guides_editor(
21826 &"
21827 block1
21828
21829
21830
21831 block2
21832 "
21833 .unindent(),
21834 cx,
21835 )
21836 .await;
21837
21838 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21839}
21840
21841#[gpui::test]
21842async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
21843 let (buffer_id, mut cx) = setup_indent_guides_editor(
21844 &"
21845 def a:
21846 \tb = 3
21847 \tif True:
21848 \t\tc = 4
21849 \t\td = 5
21850 \tprint(b)
21851 "
21852 .unindent(),
21853 cx,
21854 )
21855 .await;
21856
21857 assert_indent_guides(
21858 0..6,
21859 vec![
21860 indent_guide(buffer_id, 1, 5, 0),
21861 indent_guide(buffer_id, 3, 4, 1),
21862 ],
21863 None,
21864 &mut cx,
21865 );
21866}
21867
21868#[gpui::test]
21869async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
21870 let (buffer_id, mut cx) = setup_indent_guides_editor(
21871 &"
21872 fn main() {
21873 let a = 1;
21874 }"
21875 .unindent(),
21876 cx,
21877 )
21878 .await;
21879
21880 cx.update_editor(|editor, window, cx| {
21881 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21882 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21883 });
21884 });
21885
21886 assert_indent_guides(
21887 0..3,
21888 vec![indent_guide(buffer_id, 1, 1, 0)],
21889 Some(vec![0]),
21890 &mut cx,
21891 );
21892}
21893
21894#[gpui::test]
21895async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
21896 let (buffer_id, mut cx) = setup_indent_guides_editor(
21897 &"
21898 fn main() {
21899 if 1 == 2 {
21900 let a = 1;
21901 }
21902 }"
21903 .unindent(),
21904 cx,
21905 )
21906 .await;
21907
21908 cx.update_editor(|editor, window, cx| {
21909 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21910 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21911 });
21912 });
21913
21914 assert_indent_guides(
21915 0..4,
21916 vec![
21917 indent_guide(buffer_id, 1, 3, 0),
21918 indent_guide(buffer_id, 2, 2, 1),
21919 ],
21920 Some(vec![1]),
21921 &mut cx,
21922 );
21923
21924 cx.update_editor(|editor, window, cx| {
21925 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21926 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21927 });
21928 });
21929
21930 assert_indent_guides(
21931 0..4,
21932 vec![
21933 indent_guide(buffer_id, 1, 3, 0),
21934 indent_guide(buffer_id, 2, 2, 1),
21935 ],
21936 Some(vec![1]),
21937 &mut cx,
21938 );
21939
21940 cx.update_editor(|editor, window, cx| {
21941 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21942 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
21943 });
21944 });
21945
21946 assert_indent_guides(
21947 0..4,
21948 vec![
21949 indent_guide(buffer_id, 1, 3, 0),
21950 indent_guide(buffer_id, 2, 2, 1),
21951 ],
21952 Some(vec![0]),
21953 &mut cx,
21954 );
21955}
21956
21957#[gpui::test]
21958async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
21959 let (buffer_id, mut cx) = setup_indent_guides_editor(
21960 &"
21961 fn main() {
21962 let a = 1;
21963
21964 let b = 2;
21965 }"
21966 .unindent(),
21967 cx,
21968 )
21969 .await;
21970
21971 cx.update_editor(|editor, window, cx| {
21972 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21973 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21974 });
21975 });
21976
21977 assert_indent_guides(
21978 0..5,
21979 vec![indent_guide(buffer_id, 1, 3, 0)],
21980 Some(vec![0]),
21981 &mut cx,
21982 );
21983}
21984
21985#[gpui::test]
21986async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
21987 let (buffer_id, mut cx) = setup_indent_guides_editor(
21988 &"
21989 def m:
21990 a = 1
21991 pass"
21992 .unindent(),
21993 cx,
21994 )
21995 .await;
21996
21997 cx.update_editor(|editor, window, cx| {
21998 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21999 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
22000 });
22001 });
22002
22003 assert_indent_guides(
22004 0..3,
22005 vec![indent_guide(buffer_id, 1, 2, 0)],
22006 Some(vec![0]),
22007 &mut cx,
22008 );
22009}
22010
22011#[gpui::test]
22012async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
22013 init_test(cx, |_| {});
22014 let mut cx = EditorTestContext::new(cx).await;
22015 let text = indoc! {
22016 "
22017 impl A {
22018 fn b() {
22019 0;
22020 3;
22021 5;
22022 6;
22023 7;
22024 }
22025 }
22026 "
22027 };
22028 let base_text = indoc! {
22029 "
22030 impl A {
22031 fn b() {
22032 0;
22033 1;
22034 2;
22035 3;
22036 4;
22037 }
22038 fn c() {
22039 5;
22040 6;
22041 7;
22042 }
22043 }
22044 "
22045 };
22046
22047 cx.update_editor(|editor, window, cx| {
22048 editor.set_text(text, window, cx);
22049
22050 editor.buffer().update(cx, |multibuffer, cx| {
22051 let buffer = multibuffer.as_singleton().unwrap();
22052 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
22053
22054 multibuffer.set_all_diff_hunks_expanded(cx);
22055 multibuffer.add_diff(diff, cx);
22056
22057 buffer.read(cx).remote_id()
22058 })
22059 });
22060 cx.run_until_parked();
22061
22062 cx.assert_state_with_diff(
22063 indoc! { "
22064 impl A {
22065 fn b() {
22066 0;
22067 - 1;
22068 - 2;
22069 3;
22070 - 4;
22071 - }
22072 - fn c() {
22073 5;
22074 6;
22075 7;
22076 }
22077 }
22078 ˇ"
22079 }
22080 .to_string(),
22081 );
22082
22083 let mut actual_guides = cx.update_editor(|editor, window, cx| {
22084 editor
22085 .snapshot(window, cx)
22086 .buffer_snapshot()
22087 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
22088 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
22089 .collect::<Vec<_>>()
22090 });
22091 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
22092 assert_eq!(
22093 actual_guides,
22094 vec![
22095 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
22096 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
22097 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
22098 ]
22099 );
22100}
22101
22102#[gpui::test]
22103async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
22104 init_test(cx, |_| {});
22105 let mut cx = EditorTestContext::new(cx).await;
22106
22107 let diff_base = r#"
22108 a
22109 b
22110 c
22111 "#
22112 .unindent();
22113
22114 cx.set_state(
22115 &r#"
22116 ˇA
22117 b
22118 C
22119 "#
22120 .unindent(),
22121 );
22122 cx.set_head_text(&diff_base);
22123 cx.update_editor(|editor, window, cx| {
22124 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22125 });
22126 executor.run_until_parked();
22127
22128 let both_hunks_expanded = r#"
22129 - a
22130 + ˇA
22131 b
22132 - c
22133 + C
22134 "#
22135 .unindent();
22136
22137 cx.assert_state_with_diff(both_hunks_expanded.clone());
22138
22139 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22140 let snapshot = editor.snapshot(window, cx);
22141 let hunks = editor
22142 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22143 .collect::<Vec<_>>();
22144 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22145 hunks
22146 .into_iter()
22147 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22148 .collect::<Vec<_>>()
22149 });
22150 assert_eq!(hunk_ranges.len(), 2);
22151
22152 cx.update_editor(|editor, _, cx| {
22153 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22154 });
22155 executor.run_until_parked();
22156
22157 let second_hunk_expanded = r#"
22158 ˇA
22159 b
22160 - c
22161 + C
22162 "#
22163 .unindent();
22164
22165 cx.assert_state_with_diff(second_hunk_expanded);
22166
22167 cx.update_editor(|editor, _, cx| {
22168 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22169 });
22170 executor.run_until_parked();
22171
22172 cx.assert_state_with_diff(both_hunks_expanded.clone());
22173
22174 cx.update_editor(|editor, _, cx| {
22175 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22176 });
22177 executor.run_until_parked();
22178
22179 let first_hunk_expanded = r#"
22180 - a
22181 + ˇA
22182 b
22183 C
22184 "#
22185 .unindent();
22186
22187 cx.assert_state_with_diff(first_hunk_expanded);
22188
22189 cx.update_editor(|editor, _, cx| {
22190 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22191 });
22192 executor.run_until_parked();
22193
22194 cx.assert_state_with_diff(both_hunks_expanded);
22195
22196 cx.set_state(
22197 &r#"
22198 ˇA
22199 b
22200 "#
22201 .unindent(),
22202 );
22203 cx.run_until_parked();
22204
22205 // TODO this cursor position seems bad
22206 cx.assert_state_with_diff(
22207 r#"
22208 - ˇa
22209 + A
22210 b
22211 "#
22212 .unindent(),
22213 );
22214
22215 cx.update_editor(|editor, window, cx| {
22216 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22217 });
22218
22219 cx.assert_state_with_diff(
22220 r#"
22221 - ˇa
22222 + A
22223 b
22224 - c
22225 "#
22226 .unindent(),
22227 );
22228
22229 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22230 let snapshot = editor.snapshot(window, cx);
22231 let hunks = editor
22232 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22233 .collect::<Vec<_>>();
22234 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22235 hunks
22236 .into_iter()
22237 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22238 .collect::<Vec<_>>()
22239 });
22240 assert_eq!(hunk_ranges.len(), 2);
22241
22242 cx.update_editor(|editor, _, cx| {
22243 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22244 });
22245 executor.run_until_parked();
22246
22247 cx.assert_state_with_diff(
22248 r#"
22249 - ˇa
22250 + A
22251 b
22252 "#
22253 .unindent(),
22254 );
22255}
22256
22257#[gpui::test]
22258async fn test_toggle_deletion_hunk_at_start_of_file(
22259 executor: BackgroundExecutor,
22260 cx: &mut TestAppContext,
22261) {
22262 init_test(cx, |_| {});
22263 let mut cx = EditorTestContext::new(cx).await;
22264
22265 let diff_base = r#"
22266 a
22267 b
22268 c
22269 "#
22270 .unindent();
22271
22272 cx.set_state(
22273 &r#"
22274 ˇb
22275 c
22276 "#
22277 .unindent(),
22278 );
22279 cx.set_head_text(&diff_base);
22280 cx.update_editor(|editor, window, cx| {
22281 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22282 });
22283 executor.run_until_parked();
22284
22285 let hunk_expanded = r#"
22286 - a
22287 ˇb
22288 c
22289 "#
22290 .unindent();
22291
22292 cx.assert_state_with_diff(hunk_expanded.clone());
22293
22294 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22295 let snapshot = editor.snapshot(window, cx);
22296 let hunks = editor
22297 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22298 .collect::<Vec<_>>();
22299 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22300 hunks
22301 .into_iter()
22302 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22303 .collect::<Vec<_>>()
22304 });
22305 assert_eq!(hunk_ranges.len(), 1);
22306
22307 cx.update_editor(|editor, _, cx| {
22308 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22309 });
22310 executor.run_until_parked();
22311
22312 let hunk_collapsed = r#"
22313 ˇb
22314 c
22315 "#
22316 .unindent();
22317
22318 cx.assert_state_with_diff(hunk_collapsed);
22319
22320 cx.update_editor(|editor, _, cx| {
22321 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22322 });
22323 executor.run_until_parked();
22324
22325 cx.assert_state_with_diff(hunk_expanded);
22326}
22327
22328#[gpui::test]
22329async fn test_expand_first_line_diff_hunk_keeps_deleted_lines_visible(
22330 executor: BackgroundExecutor,
22331 cx: &mut TestAppContext,
22332) {
22333 init_test(cx, |_| {});
22334 let mut cx = EditorTestContext::new(cx).await;
22335
22336 cx.set_state("ˇnew\nsecond\nthird\n");
22337 cx.set_head_text("old\nsecond\nthird\n");
22338 cx.update_editor(|editor, window, cx| {
22339 editor.scroll(gpui::Point { x: 0., y: 0. }, None, window, cx);
22340 });
22341 executor.run_until_parked();
22342 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
22343
22344 // Expanding a diff hunk at the first line inserts deleted lines above the first buffer line.
22345 cx.update_editor(|editor, window, cx| {
22346 let snapshot = editor.snapshot(window, cx);
22347 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22348 let hunks = editor
22349 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22350 .collect::<Vec<_>>();
22351 assert_eq!(hunks.len(), 1);
22352 let hunk_range = Anchor::range_in_buffer(excerpt_id, hunks[0].buffer_range.clone());
22353 editor.toggle_single_diff_hunk(hunk_range, cx)
22354 });
22355 executor.run_until_parked();
22356 cx.assert_state_with_diff("- old\n+ ˇnew\n second\n third\n".to_string());
22357
22358 // Keep the editor scrolled to the top so the full hunk remains visible.
22359 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
22360}
22361
22362#[gpui::test]
22363async fn test_display_diff_hunks(cx: &mut TestAppContext) {
22364 init_test(cx, |_| {});
22365
22366 let fs = FakeFs::new(cx.executor());
22367 fs.insert_tree(
22368 path!("/test"),
22369 json!({
22370 ".git": {},
22371 "file-1": "ONE\n",
22372 "file-2": "TWO\n",
22373 "file-3": "THREE\n",
22374 }),
22375 )
22376 .await;
22377
22378 fs.set_head_for_repo(
22379 path!("/test/.git").as_ref(),
22380 &[
22381 ("file-1", "one\n".into()),
22382 ("file-2", "two\n".into()),
22383 ("file-3", "three\n".into()),
22384 ],
22385 "deadbeef",
22386 );
22387
22388 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
22389 let mut buffers = vec![];
22390 for i in 1..=3 {
22391 let buffer = project
22392 .update(cx, |project, cx| {
22393 let path = format!(path!("/test/file-{}"), i);
22394 project.open_local_buffer(path, cx)
22395 })
22396 .await
22397 .unwrap();
22398 buffers.push(buffer);
22399 }
22400
22401 let multibuffer = cx.new(|cx| {
22402 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
22403 multibuffer.set_all_diff_hunks_expanded(cx);
22404 for buffer in &buffers {
22405 let snapshot = buffer.read(cx).snapshot();
22406 multibuffer.set_excerpts_for_path(
22407 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
22408 buffer.clone(),
22409 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
22410 2,
22411 cx,
22412 );
22413 }
22414 multibuffer
22415 });
22416
22417 let editor = cx.add_window(|window, cx| {
22418 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
22419 });
22420 cx.run_until_parked();
22421
22422 let snapshot = editor
22423 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22424 .unwrap();
22425 let hunks = snapshot
22426 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
22427 .map(|hunk| match hunk {
22428 DisplayDiffHunk::Unfolded {
22429 display_row_range, ..
22430 } => display_row_range,
22431 DisplayDiffHunk::Folded { .. } => unreachable!(),
22432 })
22433 .collect::<Vec<_>>();
22434 assert_eq!(
22435 hunks,
22436 [
22437 DisplayRow(2)..DisplayRow(4),
22438 DisplayRow(7)..DisplayRow(9),
22439 DisplayRow(12)..DisplayRow(14),
22440 ]
22441 );
22442}
22443
22444#[gpui::test]
22445async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
22446 init_test(cx, |_| {});
22447
22448 let mut cx = EditorTestContext::new(cx).await;
22449 cx.set_head_text(indoc! { "
22450 one
22451 two
22452 three
22453 four
22454 five
22455 "
22456 });
22457 cx.set_index_text(indoc! { "
22458 one
22459 two
22460 three
22461 four
22462 five
22463 "
22464 });
22465 cx.set_state(indoc! {"
22466 one
22467 TWO
22468 ˇTHREE
22469 FOUR
22470 five
22471 "});
22472 cx.run_until_parked();
22473 cx.update_editor(|editor, window, cx| {
22474 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22475 });
22476 cx.run_until_parked();
22477 cx.assert_index_text(Some(indoc! {"
22478 one
22479 TWO
22480 THREE
22481 FOUR
22482 five
22483 "}));
22484 cx.set_state(indoc! { "
22485 one
22486 TWO
22487 ˇTHREE-HUNDRED
22488 FOUR
22489 five
22490 "});
22491 cx.run_until_parked();
22492 cx.update_editor(|editor, window, cx| {
22493 let snapshot = editor.snapshot(window, cx);
22494 let hunks = editor
22495 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22496 .collect::<Vec<_>>();
22497 assert_eq!(hunks.len(), 1);
22498 assert_eq!(
22499 hunks[0].status(),
22500 DiffHunkStatus {
22501 kind: DiffHunkStatusKind::Modified,
22502 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
22503 }
22504 );
22505
22506 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22507 });
22508 cx.run_until_parked();
22509 cx.assert_index_text(Some(indoc! {"
22510 one
22511 TWO
22512 THREE-HUNDRED
22513 FOUR
22514 five
22515 "}));
22516}
22517
22518#[gpui::test]
22519fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
22520 init_test(cx, |_| {});
22521
22522 let editor = cx.add_window(|window, cx| {
22523 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
22524 build_editor(buffer, window, cx)
22525 });
22526
22527 let render_args = Arc::new(Mutex::new(None));
22528 let snapshot = editor
22529 .update(cx, |editor, window, cx| {
22530 let snapshot = editor.buffer().read(cx).snapshot(cx);
22531 let range =
22532 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
22533
22534 struct RenderArgs {
22535 row: MultiBufferRow,
22536 folded: bool,
22537 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
22538 }
22539
22540 let crease = Crease::inline(
22541 range,
22542 FoldPlaceholder::test(),
22543 {
22544 let toggle_callback = render_args.clone();
22545 move |row, folded, callback, _window, _cx| {
22546 *toggle_callback.lock() = Some(RenderArgs {
22547 row,
22548 folded,
22549 callback,
22550 });
22551 div()
22552 }
22553 },
22554 |_row, _folded, _window, _cx| div(),
22555 );
22556
22557 editor.insert_creases(Some(crease), cx);
22558 let snapshot = editor.snapshot(window, cx);
22559 let _div =
22560 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
22561 snapshot
22562 })
22563 .unwrap();
22564
22565 let render_args = render_args.lock().take().unwrap();
22566 assert_eq!(render_args.row, MultiBufferRow(1));
22567 assert!(!render_args.folded);
22568 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22569
22570 cx.update_window(*editor, |_, window, cx| {
22571 (render_args.callback)(true, window, cx)
22572 })
22573 .unwrap();
22574 let snapshot = editor
22575 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22576 .unwrap();
22577 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
22578
22579 cx.update_window(*editor, |_, window, cx| {
22580 (render_args.callback)(false, window, cx)
22581 })
22582 .unwrap();
22583 let snapshot = editor
22584 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22585 .unwrap();
22586 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
22587}
22588
22589#[gpui::test]
22590async fn test_input_text(cx: &mut TestAppContext) {
22591 init_test(cx, |_| {});
22592 let mut cx = EditorTestContext::new(cx).await;
22593
22594 cx.set_state(
22595 &r#"ˇone
22596 two
22597
22598 three
22599 fourˇ
22600 five
22601
22602 siˇx"#
22603 .unindent(),
22604 );
22605
22606 cx.dispatch_action(HandleInput(String::new()));
22607 cx.assert_editor_state(
22608 &r#"ˇone
22609 two
22610
22611 three
22612 fourˇ
22613 five
22614
22615 siˇx"#
22616 .unindent(),
22617 );
22618
22619 cx.dispatch_action(HandleInput("AAAA".to_string()));
22620 cx.assert_editor_state(
22621 &r#"AAAAˇone
22622 two
22623
22624 three
22625 fourAAAAˇ
22626 five
22627
22628 siAAAAˇx"#
22629 .unindent(),
22630 );
22631}
22632
22633#[gpui::test]
22634async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
22635 init_test(cx, |_| {});
22636
22637 let mut cx = EditorTestContext::new(cx).await;
22638 cx.set_state(
22639 r#"let foo = 1;
22640let foo = 2;
22641let foo = 3;
22642let fooˇ = 4;
22643let foo = 5;
22644let foo = 6;
22645let foo = 7;
22646let foo = 8;
22647let foo = 9;
22648let foo = 10;
22649let foo = 11;
22650let foo = 12;
22651let foo = 13;
22652let foo = 14;
22653let foo = 15;"#,
22654 );
22655
22656 cx.update_editor(|e, window, cx| {
22657 assert_eq!(
22658 e.next_scroll_position,
22659 NextScrollCursorCenterTopBottom::Center,
22660 "Default next scroll direction is center",
22661 );
22662
22663 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22664 assert_eq!(
22665 e.next_scroll_position,
22666 NextScrollCursorCenterTopBottom::Top,
22667 "After center, next scroll direction should be top",
22668 );
22669
22670 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22671 assert_eq!(
22672 e.next_scroll_position,
22673 NextScrollCursorCenterTopBottom::Bottom,
22674 "After top, next scroll direction should be bottom",
22675 );
22676
22677 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22678 assert_eq!(
22679 e.next_scroll_position,
22680 NextScrollCursorCenterTopBottom::Center,
22681 "After bottom, scrolling should start over",
22682 );
22683
22684 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
22685 assert_eq!(
22686 e.next_scroll_position,
22687 NextScrollCursorCenterTopBottom::Top,
22688 "Scrolling continues if retriggered fast enough"
22689 );
22690 });
22691
22692 cx.executor()
22693 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
22694 cx.executor().run_until_parked();
22695 cx.update_editor(|e, _, _| {
22696 assert_eq!(
22697 e.next_scroll_position,
22698 NextScrollCursorCenterTopBottom::Center,
22699 "If scrolling is not triggered fast enough, it should reset"
22700 );
22701 });
22702}
22703
22704#[gpui::test]
22705async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
22706 init_test(cx, |_| {});
22707 let mut cx = EditorLspTestContext::new_rust(
22708 lsp::ServerCapabilities {
22709 definition_provider: Some(lsp::OneOf::Left(true)),
22710 references_provider: Some(lsp::OneOf::Left(true)),
22711 ..lsp::ServerCapabilities::default()
22712 },
22713 cx,
22714 )
22715 .await;
22716
22717 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
22718 let go_to_definition = cx
22719 .lsp
22720 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22721 move |params, _| async move {
22722 if empty_go_to_definition {
22723 Ok(None)
22724 } else {
22725 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
22726 uri: params.text_document_position_params.text_document.uri,
22727 range: lsp::Range::new(
22728 lsp::Position::new(4, 3),
22729 lsp::Position::new(4, 6),
22730 ),
22731 })))
22732 }
22733 },
22734 );
22735 let references = cx
22736 .lsp
22737 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22738 Ok(Some(vec![lsp::Location {
22739 uri: params.text_document_position.text_document.uri,
22740 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
22741 }]))
22742 });
22743 (go_to_definition, references)
22744 };
22745
22746 cx.set_state(
22747 &r#"fn one() {
22748 let mut a = ˇtwo();
22749 }
22750
22751 fn two() {}"#
22752 .unindent(),
22753 );
22754 set_up_lsp_handlers(false, &mut cx);
22755 let navigated = cx
22756 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22757 .await
22758 .expect("Failed to navigate to definition");
22759 assert_eq!(
22760 navigated,
22761 Navigated::Yes,
22762 "Should have navigated to definition from the GetDefinition response"
22763 );
22764 cx.assert_editor_state(
22765 &r#"fn one() {
22766 let mut a = two();
22767 }
22768
22769 fn «twoˇ»() {}"#
22770 .unindent(),
22771 );
22772
22773 let editors = cx.update_workspace(|workspace, _, cx| {
22774 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22775 });
22776 cx.update_editor(|_, _, test_editor_cx| {
22777 assert_eq!(
22778 editors.len(),
22779 1,
22780 "Initially, only one, test, editor should be open in the workspace"
22781 );
22782 assert_eq!(
22783 test_editor_cx.entity(),
22784 editors.last().expect("Asserted len is 1").clone()
22785 );
22786 });
22787
22788 set_up_lsp_handlers(true, &mut cx);
22789 let navigated = cx
22790 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22791 .await
22792 .expect("Failed to navigate to lookup references");
22793 assert_eq!(
22794 navigated,
22795 Navigated::Yes,
22796 "Should have navigated to references as a fallback after empty GoToDefinition response"
22797 );
22798 // We should not change the selections in the existing file,
22799 // if opening another milti buffer with the references
22800 cx.assert_editor_state(
22801 &r#"fn one() {
22802 let mut a = two();
22803 }
22804
22805 fn «twoˇ»() {}"#
22806 .unindent(),
22807 );
22808 let editors = cx.update_workspace(|workspace, _, cx| {
22809 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22810 });
22811 cx.update_editor(|_, _, test_editor_cx| {
22812 assert_eq!(
22813 editors.len(),
22814 2,
22815 "After falling back to references search, we open a new editor with the results"
22816 );
22817 let references_fallback_text = editors
22818 .into_iter()
22819 .find(|new_editor| *new_editor != test_editor_cx.entity())
22820 .expect("Should have one non-test editor now")
22821 .read(test_editor_cx)
22822 .text(test_editor_cx);
22823 assert_eq!(
22824 references_fallback_text, "fn one() {\n let mut a = two();\n}",
22825 "Should use the range from the references response and not the GoToDefinition one"
22826 );
22827 });
22828}
22829
22830#[gpui::test]
22831async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
22832 init_test(cx, |_| {});
22833 cx.update(|cx| {
22834 let mut editor_settings = EditorSettings::get_global(cx).clone();
22835 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
22836 EditorSettings::override_global(editor_settings, cx);
22837 });
22838 let mut cx = EditorLspTestContext::new_rust(
22839 lsp::ServerCapabilities {
22840 definition_provider: Some(lsp::OneOf::Left(true)),
22841 references_provider: Some(lsp::OneOf::Left(true)),
22842 ..lsp::ServerCapabilities::default()
22843 },
22844 cx,
22845 )
22846 .await;
22847 let original_state = r#"fn one() {
22848 let mut a = ˇtwo();
22849 }
22850
22851 fn two() {}"#
22852 .unindent();
22853 cx.set_state(&original_state);
22854
22855 let mut go_to_definition = cx
22856 .lsp
22857 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
22858 move |_, _| async move { Ok(None) },
22859 );
22860 let _references = cx
22861 .lsp
22862 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
22863 panic!("Should not call for references with no go to definition fallback")
22864 });
22865
22866 let navigated = cx
22867 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
22868 .await
22869 .expect("Failed to navigate to lookup references");
22870 go_to_definition
22871 .next()
22872 .await
22873 .expect("Should have called the go_to_definition handler");
22874
22875 assert_eq!(
22876 navigated,
22877 Navigated::No,
22878 "Should have navigated to references as a fallback after empty GoToDefinition response"
22879 );
22880 cx.assert_editor_state(&original_state);
22881 let editors = cx.update_workspace(|workspace, _, cx| {
22882 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22883 });
22884 cx.update_editor(|_, _, _| {
22885 assert_eq!(
22886 editors.len(),
22887 1,
22888 "After unsuccessful fallback, no other editor should have been opened"
22889 );
22890 });
22891}
22892
22893#[gpui::test]
22894async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
22895 init_test(cx, |_| {});
22896 let mut cx = EditorLspTestContext::new_rust(
22897 lsp::ServerCapabilities {
22898 references_provider: Some(lsp::OneOf::Left(true)),
22899 ..lsp::ServerCapabilities::default()
22900 },
22901 cx,
22902 )
22903 .await;
22904
22905 cx.set_state(
22906 &r#"
22907 fn one() {
22908 let mut a = two();
22909 }
22910
22911 fn ˇtwo() {}"#
22912 .unindent(),
22913 );
22914 cx.lsp
22915 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22916 Ok(Some(vec![
22917 lsp::Location {
22918 uri: params.text_document_position.text_document.uri.clone(),
22919 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22920 },
22921 lsp::Location {
22922 uri: params.text_document_position.text_document.uri,
22923 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
22924 },
22925 ]))
22926 });
22927 let navigated = cx
22928 .update_editor(|editor, window, cx| {
22929 editor.find_all_references(&FindAllReferences::default(), window, cx)
22930 })
22931 .unwrap()
22932 .await
22933 .expect("Failed to navigate to references");
22934 assert_eq!(
22935 navigated,
22936 Navigated::Yes,
22937 "Should have navigated to references from the FindAllReferences response"
22938 );
22939 cx.assert_editor_state(
22940 &r#"fn one() {
22941 let mut a = two();
22942 }
22943
22944 fn ˇtwo() {}"#
22945 .unindent(),
22946 );
22947
22948 let editors = cx.update_workspace(|workspace, _, cx| {
22949 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22950 });
22951 cx.update_editor(|_, _, _| {
22952 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
22953 });
22954
22955 cx.set_state(
22956 &r#"fn one() {
22957 let mut a = ˇtwo();
22958 }
22959
22960 fn two() {}"#
22961 .unindent(),
22962 );
22963 let navigated = cx
22964 .update_editor(|editor, window, cx| {
22965 editor.find_all_references(&FindAllReferences::default(), window, cx)
22966 })
22967 .unwrap()
22968 .await
22969 .expect("Failed to navigate to references");
22970 assert_eq!(
22971 navigated,
22972 Navigated::Yes,
22973 "Should have navigated to references from the FindAllReferences response"
22974 );
22975 cx.assert_editor_state(
22976 &r#"fn one() {
22977 let mut a = ˇtwo();
22978 }
22979
22980 fn two() {}"#
22981 .unindent(),
22982 );
22983 let editors = cx.update_workspace(|workspace, _, cx| {
22984 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22985 });
22986 cx.update_editor(|_, _, _| {
22987 assert_eq!(
22988 editors.len(),
22989 2,
22990 "should have re-used the previous multibuffer"
22991 );
22992 });
22993
22994 cx.set_state(
22995 &r#"fn one() {
22996 let mut a = ˇtwo();
22997 }
22998 fn three() {}
22999 fn two() {}"#
23000 .unindent(),
23001 );
23002 cx.lsp
23003 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
23004 Ok(Some(vec![
23005 lsp::Location {
23006 uri: params.text_document_position.text_document.uri.clone(),
23007 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
23008 },
23009 lsp::Location {
23010 uri: params.text_document_position.text_document.uri,
23011 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
23012 },
23013 ]))
23014 });
23015 let navigated = cx
23016 .update_editor(|editor, window, cx| {
23017 editor.find_all_references(&FindAllReferences::default(), window, cx)
23018 })
23019 .unwrap()
23020 .await
23021 .expect("Failed to navigate to references");
23022 assert_eq!(
23023 navigated,
23024 Navigated::Yes,
23025 "Should have navigated to references from the FindAllReferences response"
23026 );
23027 cx.assert_editor_state(
23028 &r#"fn one() {
23029 let mut a = ˇtwo();
23030 }
23031 fn three() {}
23032 fn two() {}"#
23033 .unindent(),
23034 );
23035 let editors = cx.update_workspace(|workspace, _, cx| {
23036 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23037 });
23038 cx.update_editor(|_, _, _| {
23039 assert_eq!(
23040 editors.len(),
23041 3,
23042 "should have used a new multibuffer as offsets changed"
23043 );
23044 });
23045}
23046#[gpui::test]
23047async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
23048 init_test(cx, |_| {});
23049
23050 let language = Arc::new(Language::new(
23051 LanguageConfig::default(),
23052 Some(tree_sitter_rust::LANGUAGE.into()),
23053 ));
23054
23055 let text = r#"
23056 #[cfg(test)]
23057 mod tests() {
23058 #[test]
23059 fn runnable_1() {
23060 let a = 1;
23061 }
23062
23063 #[test]
23064 fn runnable_2() {
23065 let a = 1;
23066 let b = 2;
23067 }
23068 }
23069 "#
23070 .unindent();
23071
23072 let fs = FakeFs::new(cx.executor());
23073 fs.insert_file("/file.rs", Default::default()).await;
23074
23075 let project = Project::test(fs, ["/a".as_ref()], cx).await;
23076 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23077 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23078 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
23079 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
23080
23081 let editor = cx.new_window_entity(|window, cx| {
23082 Editor::new(
23083 EditorMode::full(),
23084 multi_buffer,
23085 Some(project.clone()),
23086 window,
23087 cx,
23088 )
23089 });
23090
23091 editor.update_in(cx, |editor, window, cx| {
23092 let snapshot = editor.buffer().read(cx).snapshot(cx);
23093 editor.tasks.insert(
23094 (buffer.read(cx).remote_id(), 3),
23095 RunnableTasks {
23096 templates: vec![],
23097 offset: snapshot.anchor_before(MultiBufferOffset(43)),
23098 column: 0,
23099 extra_variables: HashMap::default(),
23100 context_range: BufferOffset(43)..BufferOffset(85),
23101 },
23102 );
23103 editor.tasks.insert(
23104 (buffer.read(cx).remote_id(), 8),
23105 RunnableTasks {
23106 templates: vec![],
23107 offset: snapshot.anchor_before(MultiBufferOffset(86)),
23108 column: 0,
23109 extra_variables: HashMap::default(),
23110 context_range: BufferOffset(86)..BufferOffset(191),
23111 },
23112 );
23113
23114 // Test finding task when cursor is inside function body
23115 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23116 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
23117 });
23118 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23119 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
23120
23121 // Test finding task when cursor is on function name
23122 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23123 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
23124 });
23125 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23126 assert_eq!(row, 8, "Should find task when cursor is on function name");
23127 });
23128}
23129
23130#[gpui::test]
23131async fn test_folding_buffers(cx: &mut TestAppContext) {
23132 init_test(cx, |_| {});
23133
23134 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23135 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
23136 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
23137
23138 let fs = FakeFs::new(cx.executor());
23139 fs.insert_tree(
23140 path!("/a"),
23141 json!({
23142 "first.rs": sample_text_1,
23143 "second.rs": sample_text_2,
23144 "third.rs": sample_text_3,
23145 }),
23146 )
23147 .await;
23148 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23149 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23150 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23151 let worktree = project.update(cx, |project, cx| {
23152 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23153 assert_eq!(worktrees.len(), 1);
23154 worktrees.pop().unwrap()
23155 });
23156 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23157
23158 let buffer_1 = project
23159 .update(cx, |project, cx| {
23160 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23161 })
23162 .await
23163 .unwrap();
23164 let buffer_2 = project
23165 .update(cx, |project, cx| {
23166 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23167 })
23168 .await
23169 .unwrap();
23170 let buffer_3 = project
23171 .update(cx, |project, cx| {
23172 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23173 })
23174 .await
23175 .unwrap();
23176
23177 let multi_buffer = cx.new(|cx| {
23178 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23179 multi_buffer.push_excerpts(
23180 buffer_1.clone(),
23181 [
23182 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23183 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23184 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23185 ],
23186 cx,
23187 );
23188 multi_buffer.push_excerpts(
23189 buffer_2.clone(),
23190 [
23191 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23192 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23193 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23194 ],
23195 cx,
23196 );
23197 multi_buffer.push_excerpts(
23198 buffer_3.clone(),
23199 [
23200 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23201 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23202 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23203 ],
23204 cx,
23205 );
23206 multi_buffer
23207 });
23208 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23209 Editor::new(
23210 EditorMode::full(),
23211 multi_buffer.clone(),
23212 Some(project.clone()),
23213 window,
23214 cx,
23215 )
23216 });
23217
23218 assert_eq!(
23219 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23220 "\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",
23221 );
23222
23223 multi_buffer_editor.update(cx, |editor, cx| {
23224 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
23225 });
23226 assert_eq!(
23227 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23228 "\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",
23229 "After folding the first buffer, its text should not be displayed"
23230 );
23231
23232 multi_buffer_editor.update(cx, |editor, cx| {
23233 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
23234 });
23235 assert_eq!(
23236 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23237 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
23238 "After folding the second buffer, its text should not be displayed"
23239 );
23240
23241 multi_buffer_editor.update(cx, |editor, cx| {
23242 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23243 });
23244 assert_eq!(
23245 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23246 "\n\n\n\n\n",
23247 "After folding the third buffer, its text should not be displayed"
23248 );
23249
23250 // Emulate selection inside the fold logic, that should work
23251 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23252 editor
23253 .snapshot(window, cx)
23254 .next_line_boundary(Point::new(0, 4));
23255 });
23256
23257 multi_buffer_editor.update(cx, |editor, cx| {
23258 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23259 });
23260 assert_eq!(
23261 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23262 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
23263 "After unfolding the second buffer, its text should be displayed"
23264 );
23265
23266 // Typing inside of buffer 1 causes that buffer to be unfolded.
23267 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23268 assert_eq!(
23269 multi_buffer
23270 .read(cx)
23271 .snapshot(cx)
23272 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
23273 .collect::<String>(),
23274 "bbbb"
23275 );
23276 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23277 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
23278 });
23279 editor.handle_input("B", window, cx);
23280 });
23281
23282 assert_eq!(
23283 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23284 "\n\naaaa\nBbbbb\ncccc\n\n\nffff\ngggg\n\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
23285 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
23286 );
23287
23288 multi_buffer_editor.update(cx, |editor, cx| {
23289 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23290 });
23291 assert_eq!(
23292 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23293 "\n\naaaa\nBbbbb\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",
23294 "After unfolding the all buffers, all original text should be displayed"
23295 );
23296}
23297
23298#[gpui::test]
23299async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
23300 init_test(cx, |_| {});
23301
23302 let sample_text_1 = "1111\n2222\n3333".to_string();
23303 let sample_text_2 = "4444\n5555\n6666".to_string();
23304 let sample_text_3 = "7777\n8888\n9999".to_string();
23305
23306 let fs = FakeFs::new(cx.executor());
23307 fs.insert_tree(
23308 path!("/a"),
23309 json!({
23310 "first.rs": sample_text_1,
23311 "second.rs": sample_text_2,
23312 "third.rs": sample_text_3,
23313 }),
23314 )
23315 .await;
23316 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23317 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23318 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23319 let worktree = project.update(cx, |project, cx| {
23320 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23321 assert_eq!(worktrees.len(), 1);
23322 worktrees.pop().unwrap()
23323 });
23324 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23325
23326 let buffer_1 = project
23327 .update(cx, |project, cx| {
23328 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23329 })
23330 .await
23331 .unwrap();
23332 let buffer_2 = project
23333 .update(cx, |project, cx| {
23334 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23335 })
23336 .await
23337 .unwrap();
23338 let buffer_3 = project
23339 .update(cx, |project, cx| {
23340 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23341 })
23342 .await
23343 .unwrap();
23344
23345 let multi_buffer = cx.new(|cx| {
23346 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23347 multi_buffer.push_excerpts(
23348 buffer_1.clone(),
23349 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23350 cx,
23351 );
23352 multi_buffer.push_excerpts(
23353 buffer_2.clone(),
23354 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23355 cx,
23356 );
23357 multi_buffer.push_excerpts(
23358 buffer_3.clone(),
23359 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23360 cx,
23361 );
23362 multi_buffer
23363 });
23364
23365 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23366 Editor::new(
23367 EditorMode::full(),
23368 multi_buffer,
23369 Some(project.clone()),
23370 window,
23371 cx,
23372 )
23373 });
23374
23375 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
23376 assert_eq!(
23377 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23378 full_text,
23379 );
23380
23381 multi_buffer_editor.update(cx, |editor, cx| {
23382 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
23383 });
23384 assert_eq!(
23385 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23386 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
23387 "After folding the first buffer, its text should not be displayed"
23388 );
23389
23390 multi_buffer_editor.update(cx, |editor, cx| {
23391 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
23392 });
23393
23394 assert_eq!(
23395 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23396 "\n\n\n\n\n\n7777\n8888\n9999",
23397 "After folding the second buffer, its text should not be displayed"
23398 );
23399
23400 multi_buffer_editor.update(cx, |editor, cx| {
23401 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23402 });
23403 assert_eq!(
23404 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23405 "\n\n\n\n\n",
23406 "After folding the third buffer, its text should not be displayed"
23407 );
23408
23409 multi_buffer_editor.update(cx, |editor, cx| {
23410 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23411 });
23412 assert_eq!(
23413 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23414 "\n\n\n\n4444\n5555\n6666\n\n",
23415 "After unfolding the second buffer, its text should be displayed"
23416 );
23417
23418 multi_buffer_editor.update(cx, |editor, cx| {
23419 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
23420 });
23421 assert_eq!(
23422 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23423 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
23424 "After unfolding the first buffer, its text should be displayed"
23425 );
23426
23427 multi_buffer_editor.update(cx, |editor, cx| {
23428 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23429 });
23430 assert_eq!(
23431 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23432 full_text,
23433 "After unfolding all buffers, all original text should be displayed"
23434 );
23435}
23436
23437#[gpui::test]
23438async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
23439 init_test(cx, |_| {});
23440
23441 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23442
23443 let fs = FakeFs::new(cx.executor());
23444 fs.insert_tree(
23445 path!("/a"),
23446 json!({
23447 "main.rs": sample_text,
23448 }),
23449 )
23450 .await;
23451 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23452 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23453 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23454 let worktree = project.update(cx, |project, cx| {
23455 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23456 assert_eq!(worktrees.len(), 1);
23457 worktrees.pop().unwrap()
23458 });
23459 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23460
23461 let buffer_1 = project
23462 .update(cx, |project, cx| {
23463 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23464 })
23465 .await
23466 .unwrap();
23467
23468 let multi_buffer = cx.new(|cx| {
23469 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23470 multi_buffer.push_excerpts(
23471 buffer_1.clone(),
23472 [ExcerptRange::new(
23473 Point::new(0, 0)
23474 ..Point::new(
23475 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
23476 0,
23477 ),
23478 )],
23479 cx,
23480 );
23481 multi_buffer
23482 });
23483 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23484 Editor::new(
23485 EditorMode::full(),
23486 multi_buffer,
23487 Some(project.clone()),
23488 window,
23489 cx,
23490 )
23491 });
23492
23493 let selection_range = Point::new(1, 0)..Point::new(2, 0);
23494 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23495 enum TestHighlight {}
23496 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
23497 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
23498 editor.highlight_text::<TestHighlight>(
23499 vec![highlight_range.clone()],
23500 HighlightStyle::color(Hsla::green()),
23501 cx,
23502 );
23503 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23504 s.select_ranges(Some(highlight_range))
23505 });
23506 });
23507
23508 let full_text = format!("\n\n{sample_text}");
23509 assert_eq!(
23510 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23511 full_text,
23512 );
23513}
23514
23515#[gpui::test]
23516async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
23517 init_test(cx, |_| {});
23518 cx.update(|cx| {
23519 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
23520 "keymaps/default-linux.json",
23521 cx,
23522 )
23523 .unwrap();
23524 cx.bind_keys(default_key_bindings);
23525 });
23526
23527 let (editor, cx) = cx.add_window_view(|window, cx| {
23528 let multi_buffer = MultiBuffer::build_multi(
23529 [
23530 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
23531 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
23532 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
23533 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
23534 ],
23535 cx,
23536 );
23537 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
23538
23539 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
23540 // fold all but the second buffer, so that we test navigating between two
23541 // adjacent folded buffers, as well as folded buffers at the start and
23542 // end the multibuffer
23543 editor.fold_buffer(buffer_ids[0], cx);
23544 editor.fold_buffer(buffer_ids[2], cx);
23545 editor.fold_buffer(buffer_ids[3], cx);
23546
23547 editor
23548 });
23549 cx.simulate_resize(size(px(1000.), px(1000.)));
23550
23551 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
23552 cx.assert_excerpts_with_selections(indoc! {"
23553 [EXCERPT]
23554 ˇ[FOLDED]
23555 [EXCERPT]
23556 a1
23557 b1
23558 [EXCERPT]
23559 [FOLDED]
23560 [EXCERPT]
23561 [FOLDED]
23562 "
23563 });
23564 cx.simulate_keystroke("down");
23565 cx.assert_excerpts_with_selections(indoc! {"
23566 [EXCERPT]
23567 [FOLDED]
23568 [EXCERPT]
23569 ˇa1
23570 b1
23571 [EXCERPT]
23572 [FOLDED]
23573 [EXCERPT]
23574 [FOLDED]
23575 "
23576 });
23577 cx.simulate_keystroke("down");
23578 cx.assert_excerpts_with_selections(indoc! {"
23579 [EXCERPT]
23580 [FOLDED]
23581 [EXCERPT]
23582 a1
23583 ˇb1
23584 [EXCERPT]
23585 [FOLDED]
23586 [EXCERPT]
23587 [FOLDED]
23588 "
23589 });
23590 cx.simulate_keystroke("down");
23591 cx.assert_excerpts_with_selections(indoc! {"
23592 [EXCERPT]
23593 [FOLDED]
23594 [EXCERPT]
23595 a1
23596 b1
23597 ˇ[EXCERPT]
23598 [FOLDED]
23599 [EXCERPT]
23600 [FOLDED]
23601 "
23602 });
23603 cx.simulate_keystroke("down");
23604 cx.assert_excerpts_with_selections(indoc! {"
23605 [EXCERPT]
23606 [FOLDED]
23607 [EXCERPT]
23608 a1
23609 b1
23610 [EXCERPT]
23611 ˇ[FOLDED]
23612 [EXCERPT]
23613 [FOLDED]
23614 "
23615 });
23616 for _ in 0..5 {
23617 cx.simulate_keystroke("down");
23618 cx.assert_excerpts_with_selections(indoc! {"
23619 [EXCERPT]
23620 [FOLDED]
23621 [EXCERPT]
23622 a1
23623 b1
23624 [EXCERPT]
23625 [FOLDED]
23626 [EXCERPT]
23627 ˇ[FOLDED]
23628 "
23629 });
23630 }
23631
23632 cx.simulate_keystroke("up");
23633 cx.assert_excerpts_with_selections(indoc! {"
23634 [EXCERPT]
23635 [FOLDED]
23636 [EXCERPT]
23637 a1
23638 b1
23639 [EXCERPT]
23640 ˇ[FOLDED]
23641 [EXCERPT]
23642 [FOLDED]
23643 "
23644 });
23645 cx.simulate_keystroke("up");
23646 cx.assert_excerpts_with_selections(indoc! {"
23647 [EXCERPT]
23648 [FOLDED]
23649 [EXCERPT]
23650 a1
23651 b1
23652 ˇ[EXCERPT]
23653 [FOLDED]
23654 [EXCERPT]
23655 [FOLDED]
23656 "
23657 });
23658 cx.simulate_keystroke("up");
23659 cx.assert_excerpts_with_selections(indoc! {"
23660 [EXCERPT]
23661 [FOLDED]
23662 [EXCERPT]
23663 a1
23664 ˇb1
23665 [EXCERPT]
23666 [FOLDED]
23667 [EXCERPT]
23668 [FOLDED]
23669 "
23670 });
23671 cx.simulate_keystroke("up");
23672 cx.assert_excerpts_with_selections(indoc! {"
23673 [EXCERPT]
23674 [FOLDED]
23675 [EXCERPT]
23676 ˇa1
23677 b1
23678 [EXCERPT]
23679 [FOLDED]
23680 [EXCERPT]
23681 [FOLDED]
23682 "
23683 });
23684 for _ in 0..5 {
23685 cx.simulate_keystroke("up");
23686 cx.assert_excerpts_with_selections(indoc! {"
23687 [EXCERPT]
23688 ˇ[FOLDED]
23689 [EXCERPT]
23690 a1
23691 b1
23692 [EXCERPT]
23693 [FOLDED]
23694 [EXCERPT]
23695 [FOLDED]
23696 "
23697 });
23698 }
23699}
23700
23701#[gpui::test]
23702async fn test_edit_prediction_text(cx: &mut TestAppContext) {
23703 init_test(cx, |_| {});
23704
23705 // Simple insertion
23706 assert_highlighted_edits(
23707 "Hello, world!",
23708 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
23709 true,
23710 cx,
23711 |highlighted_edits, cx| {
23712 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
23713 assert_eq!(highlighted_edits.highlights.len(), 1);
23714 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
23715 assert_eq!(
23716 highlighted_edits.highlights[0].1.background_color,
23717 Some(cx.theme().status().created_background)
23718 );
23719 },
23720 )
23721 .await;
23722
23723 // Replacement
23724 assert_highlighted_edits(
23725 "This is a test.",
23726 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
23727 false,
23728 cx,
23729 |highlighted_edits, cx| {
23730 assert_eq!(highlighted_edits.text, "That is a test.");
23731 assert_eq!(highlighted_edits.highlights.len(), 1);
23732 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
23733 assert_eq!(
23734 highlighted_edits.highlights[0].1.background_color,
23735 Some(cx.theme().status().created_background)
23736 );
23737 },
23738 )
23739 .await;
23740
23741 // Multiple edits
23742 assert_highlighted_edits(
23743 "Hello, world!",
23744 vec![
23745 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
23746 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
23747 ],
23748 false,
23749 cx,
23750 |highlighted_edits, cx| {
23751 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
23752 assert_eq!(highlighted_edits.highlights.len(), 2);
23753 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
23754 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
23755 assert_eq!(
23756 highlighted_edits.highlights[0].1.background_color,
23757 Some(cx.theme().status().created_background)
23758 );
23759 assert_eq!(
23760 highlighted_edits.highlights[1].1.background_color,
23761 Some(cx.theme().status().created_background)
23762 );
23763 },
23764 )
23765 .await;
23766
23767 // Multiple lines with edits
23768 assert_highlighted_edits(
23769 "First line\nSecond line\nThird line\nFourth line",
23770 vec![
23771 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
23772 (
23773 Point::new(2, 0)..Point::new(2, 10),
23774 "New third line".to_string(),
23775 ),
23776 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
23777 ],
23778 false,
23779 cx,
23780 |highlighted_edits, cx| {
23781 assert_eq!(
23782 highlighted_edits.text,
23783 "Second modified\nNew third line\nFourth updated line"
23784 );
23785 assert_eq!(highlighted_edits.highlights.len(), 3);
23786 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
23787 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
23788 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
23789 for highlight in &highlighted_edits.highlights {
23790 assert_eq!(
23791 highlight.1.background_color,
23792 Some(cx.theme().status().created_background)
23793 );
23794 }
23795 },
23796 )
23797 .await;
23798}
23799
23800#[gpui::test]
23801async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
23802 init_test(cx, |_| {});
23803
23804 // Deletion
23805 assert_highlighted_edits(
23806 "Hello, world!",
23807 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
23808 true,
23809 cx,
23810 |highlighted_edits, cx| {
23811 assert_eq!(highlighted_edits.text, "Hello, world!");
23812 assert_eq!(highlighted_edits.highlights.len(), 1);
23813 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
23814 assert_eq!(
23815 highlighted_edits.highlights[0].1.background_color,
23816 Some(cx.theme().status().deleted_background)
23817 );
23818 },
23819 )
23820 .await;
23821
23822 // Insertion
23823 assert_highlighted_edits(
23824 "Hello, world!",
23825 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
23826 true,
23827 cx,
23828 |highlighted_edits, cx| {
23829 assert_eq!(highlighted_edits.highlights.len(), 1);
23830 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
23831 assert_eq!(
23832 highlighted_edits.highlights[0].1.background_color,
23833 Some(cx.theme().status().created_background)
23834 );
23835 },
23836 )
23837 .await;
23838}
23839
23840async fn assert_highlighted_edits(
23841 text: &str,
23842 edits: Vec<(Range<Point>, String)>,
23843 include_deletions: bool,
23844 cx: &mut TestAppContext,
23845 assertion_fn: impl Fn(HighlightedText, &App),
23846) {
23847 let window = cx.add_window(|window, cx| {
23848 let buffer = MultiBuffer::build_simple(text, cx);
23849 Editor::new(EditorMode::full(), buffer, None, window, cx)
23850 });
23851 let cx = &mut VisualTestContext::from_window(*window, cx);
23852
23853 let (buffer, snapshot) = window
23854 .update(cx, |editor, _window, cx| {
23855 (
23856 editor.buffer().clone(),
23857 editor.buffer().read(cx).snapshot(cx),
23858 )
23859 })
23860 .unwrap();
23861
23862 let edits = edits
23863 .into_iter()
23864 .map(|(range, edit)| {
23865 (
23866 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
23867 edit,
23868 )
23869 })
23870 .collect::<Vec<_>>();
23871
23872 let text_anchor_edits = edits
23873 .clone()
23874 .into_iter()
23875 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
23876 .collect::<Vec<_>>();
23877
23878 let edit_preview = window
23879 .update(cx, |_, _window, cx| {
23880 buffer
23881 .read(cx)
23882 .as_singleton()
23883 .unwrap()
23884 .read(cx)
23885 .preview_edits(text_anchor_edits.into(), cx)
23886 })
23887 .unwrap()
23888 .await;
23889
23890 cx.update(|_window, cx| {
23891 let highlighted_edits = edit_prediction_edit_text(
23892 snapshot.as_singleton().unwrap().2,
23893 &edits,
23894 &edit_preview,
23895 include_deletions,
23896 cx,
23897 );
23898 assertion_fn(highlighted_edits, cx)
23899 });
23900}
23901
23902#[track_caller]
23903fn assert_breakpoint(
23904 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
23905 path: &Arc<Path>,
23906 expected: Vec<(u32, Breakpoint)>,
23907) {
23908 if expected.is_empty() {
23909 assert!(!breakpoints.contains_key(path), "{}", path.display());
23910 } else {
23911 let mut breakpoint = breakpoints
23912 .get(path)
23913 .unwrap()
23914 .iter()
23915 .map(|breakpoint| {
23916 (
23917 breakpoint.row,
23918 Breakpoint {
23919 message: breakpoint.message.clone(),
23920 state: breakpoint.state,
23921 condition: breakpoint.condition.clone(),
23922 hit_condition: breakpoint.hit_condition.clone(),
23923 },
23924 )
23925 })
23926 .collect::<Vec<_>>();
23927
23928 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
23929
23930 assert_eq!(expected, breakpoint);
23931 }
23932}
23933
23934fn add_log_breakpoint_at_cursor(
23935 editor: &mut Editor,
23936 log_message: &str,
23937 window: &mut Window,
23938 cx: &mut Context<Editor>,
23939) {
23940 let (anchor, bp) = editor
23941 .breakpoints_at_cursors(window, cx)
23942 .first()
23943 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
23944 .unwrap_or_else(|| {
23945 let snapshot = editor.snapshot(window, cx);
23946 let cursor_position: Point =
23947 editor.selections.newest(&snapshot.display_snapshot).head();
23948
23949 let breakpoint_position = snapshot
23950 .buffer_snapshot()
23951 .anchor_before(Point::new(cursor_position.row, 0));
23952
23953 (breakpoint_position, Breakpoint::new_log(log_message))
23954 });
23955
23956 editor.edit_breakpoint_at_anchor(
23957 anchor,
23958 bp,
23959 BreakpointEditAction::EditLogMessage(log_message.into()),
23960 cx,
23961 );
23962}
23963
23964#[gpui::test]
23965async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
23966 init_test(cx, |_| {});
23967
23968 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23969 let fs = FakeFs::new(cx.executor());
23970 fs.insert_tree(
23971 path!("/a"),
23972 json!({
23973 "main.rs": sample_text,
23974 }),
23975 )
23976 .await;
23977 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23978 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23979 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23980
23981 let fs = FakeFs::new(cx.executor());
23982 fs.insert_tree(
23983 path!("/a"),
23984 json!({
23985 "main.rs": sample_text,
23986 }),
23987 )
23988 .await;
23989 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23990 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23991 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23992 let worktree_id = workspace
23993 .update(cx, |workspace, _window, cx| {
23994 workspace.project().update(cx, |project, cx| {
23995 project.worktrees(cx).next().unwrap().read(cx).id()
23996 })
23997 })
23998 .unwrap();
23999
24000 let buffer = project
24001 .update(cx, |project, cx| {
24002 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24003 })
24004 .await
24005 .unwrap();
24006
24007 let (editor, cx) = cx.add_window_view(|window, cx| {
24008 Editor::new(
24009 EditorMode::full(),
24010 MultiBuffer::build_from_buffer(buffer, cx),
24011 Some(project.clone()),
24012 window,
24013 cx,
24014 )
24015 });
24016
24017 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24018 let abs_path = project.read_with(cx, |project, cx| {
24019 project
24020 .absolute_path(&project_path, cx)
24021 .map(Arc::from)
24022 .unwrap()
24023 });
24024
24025 // assert we can add breakpoint on the first line
24026 editor.update_in(cx, |editor, window, cx| {
24027 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24028 editor.move_to_end(&MoveToEnd, window, cx);
24029 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24030 });
24031
24032 let breakpoints = editor.update(cx, |editor, cx| {
24033 editor
24034 .breakpoint_store()
24035 .as_ref()
24036 .unwrap()
24037 .read(cx)
24038 .all_source_breakpoints(cx)
24039 });
24040
24041 assert_eq!(1, breakpoints.len());
24042 assert_breakpoint(
24043 &breakpoints,
24044 &abs_path,
24045 vec![
24046 (0, Breakpoint::new_standard()),
24047 (3, Breakpoint::new_standard()),
24048 ],
24049 );
24050
24051 editor.update_in(cx, |editor, window, cx| {
24052 editor.move_to_beginning(&MoveToBeginning, window, cx);
24053 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24054 });
24055
24056 let breakpoints = editor.update(cx, |editor, cx| {
24057 editor
24058 .breakpoint_store()
24059 .as_ref()
24060 .unwrap()
24061 .read(cx)
24062 .all_source_breakpoints(cx)
24063 });
24064
24065 assert_eq!(1, breakpoints.len());
24066 assert_breakpoint(
24067 &breakpoints,
24068 &abs_path,
24069 vec![(3, Breakpoint::new_standard())],
24070 );
24071
24072 editor.update_in(cx, |editor, window, cx| {
24073 editor.move_to_end(&MoveToEnd, window, cx);
24074 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24075 });
24076
24077 let breakpoints = editor.update(cx, |editor, cx| {
24078 editor
24079 .breakpoint_store()
24080 .as_ref()
24081 .unwrap()
24082 .read(cx)
24083 .all_source_breakpoints(cx)
24084 });
24085
24086 assert_eq!(0, breakpoints.len());
24087 assert_breakpoint(&breakpoints, &abs_path, vec![]);
24088}
24089
24090#[gpui::test]
24091async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
24092 init_test(cx, |_| {});
24093
24094 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24095
24096 let fs = FakeFs::new(cx.executor());
24097 fs.insert_tree(
24098 path!("/a"),
24099 json!({
24100 "main.rs": sample_text,
24101 }),
24102 )
24103 .await;
24104 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24105 let (workspace, cx) =
24106 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24107
24108 let worktree_id = workspace.update(cx, |workspace, cx| {
24109 workspace.project().update(cx, |project, cx| {
24110 project.worktrees(cx).next().unwrap().read(cx).id()
24111 })
24112 });
24113
24114 let buffer = project
24115 .update(cx, |project, cx| {
24116 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24117 })
24118 .await
24119 .unwrap();
24120
24121 let (editor, cx) = cx.add_window_view(|window, cx| {
24122 Editor::new(
24123 EditorMode::full(),
24124 MultiBuffer::build_from_buffer(buffer, cx),
24125 Some(project.clone()),
24126 window,
24127 cx,
24128 )
24129 });
24130
24131 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24132 let abs_path = project.read_with(cx, |project, cx| {
24133 project
24134 .absolute_path(&project_path, cx)
24135 .map(Arc::from)
24136 .unwrap()
24137 });
24138
24139 editor.update_in(cx, |editor, window, cx| {
24140 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24141 });
24142
24143 let breakpoints = editor.update(cx, |editor, cx| {
24144 editor
24145 .breakpoint_store()
24146 .as_ref()
24147 .unwrap()
24148 .read(cx)
24149 .all_source_breakpoints(cx)
24150 });
24151
24152 assert_breakpoint(
24153 &breakpoints,
24154 &abs_path,
24155 vec![(0, Breakpoint::new_log("hello world"))],
24156 );
24157
24158 // Removing a log message from a log breakpoint should remove it
24159 editor.update_in(cx, |editor, window, cx| {
24160 add_log_breakpoint_at_cursor(editor, "", window, cx);
24161 });
24162
24163 let breakpoints = editor.update(cx, |editor, cx| {
24164 editor
24165 .breakpoint_store()
24166 .as_ref()
24167 .unwrap()
24168 .read(cx)
24169 .all_source_breakpoints(cx)
24170 });
24171
24172 assert_breakpoint(&breakpoints, &abs_path, vec![]);
24173
24174 editor.update_in(cx, |editor, window, cx| {
24175 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24176 editor.move_to_end(&MoveToEnd, window, cx);
24177 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24178 // Not adding a log message to a standard breakpoint shouldn't remove it
24179 add_log_breakpoint_at_cursor(editor, "", window, cx);
24180 });
24181
24182 let breakpoints = editor.update(cx, |editor, cx| {
24183 editor
24184 .breakpoint_store()
24185 .as_ref()
24186 .unwrap()
24187 .read(cx)
24188 .all_source_breakpoints(cx)
24189 });
24190
24191 assert_breakpoint(
24192 &breakpoints,
24193 &abs_path,
24194 vec![
24195 (0, Breakpoint::new_standard()),
24196 (3, Breakpoint::new_standard()),
24197 ],
24198 );
24199
24200 editor.update_in(cx, |editor, window, cx| {
24201 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24202 });
24203
24204 let breakpoints = editor.update(cx, |editor, cx| {
24205 editor
24206 .breakpoint_store()
24207 .as_ref()
24208 .unwrap()
24209 .read(cx)
24210 .all_source_breakpoints(cx)
24211 });
24212
24213 assert_breakpoint(
24214 &breakpoints,
24215 &abs_path,
24216 vec![
24217 (0, Breakpoint::new_standard()),
24218 (3, Breakpoint::new_log("hello world")),
24219 ],
24220 );
24221
24222 editor.update_in(cx, |editor, window, cx| {
24223 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
24224 });
24225
24226 let breakpoints = editor.update(cx, |editor, cx| {
24227 editor
24228 .breakpoint_store()
24229 .as_ref()
24230 .unwrap()
24231 .read(cx)
24232 .all_source_breakpoints(cx)
24233 });
24234
24235 assert_breakpoint(
24236 &breakpoints,
24237 &abs_path,
24238 vec![
24239 (0, Breakpoint::new_standard()),
24240 (3, Breakpoint::new_log("hello Earth!!")),
24241 ],
24242 );
24243}
24244
24245/// This also tests that Editor::breakpoint_at_cursor_head is working properly
24246/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
24247/// or when breakpoints were placed out of order. This tests for a regression too
24248#[gpui::test]
24249async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
24250 init_test(cx, |_| {});
24251
24252 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24253 let fs = FakeFs::new(cx.executor());
24254 fs.insert_tree(
24255 path!("/a"),
24256 json!({
24257 "main.rs": sample_text,
24258 }),
24259 )
24260 .await;
24261 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24262 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24263 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24264
24265 let fs = FakeFs::new(cx.executor());
24266 fs.insert_tree(
24267 path!("/a"),
24268 json!({
24269 "main.rs": sample_text,
24270 }),
24271 )
24272 .await;
24273 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24274 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24275 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24276 let worktree_id = workspace
24277 .update(cx, |workspace, _window, cx| {
24278 workspace.project().update(cx, |project, cx| {
24279 project.worktrees(cx).next().unwrap().read(cx).id()
24280 })
24281 })
24282 .unwrap();
24283
24284 let buffer = project
24285 .update(cx, |project, cx| {
24286 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24287 })
24288 .await
24289 .unwrap();
24290
24291 let (editor, cx) = cx.add_window_view(|window, cx| {
24292 Editor::new(
24293 EditorMode::full(),
24294 MultiBuffer::build_from_buffer(buffer, cx),
24295 Some(project.clone()),
24296 window,
24297 cx,
24298 )
24299 });
24300
24301 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24302 let abs_path = project.read_with(cx, |project, cx| {
24303 project
24304 .absolute_path(&project_path, cx)
24305 .map(Arc::from)
24306 .unwrap()
24307 });
24308
24309 // assert we can add breakpoint on the first line
24310 editor.update_in(cx, |editor, window, cx| {
24311 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24312 editor.move_to_end(&MoveToEnd, window, cx);
24313 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24314 editor.move_up(&MoveUp, window, cx);
24315 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24316 });
24317
24318 let breakpoints = editor.update(cx, |editor, cx| {
24319 editor
24320 .breakpoint_store()
24321 .as_ref()
24322 .unwrap()
24323 .read(cx)
24324 .all_source_breakpoints(cx)
24325 });
24326
24327 assert_eq!(1, breakpoints.len());
24328 assert_breakpoint(
24329 &breakpoints,
24330 &abs_path,
24331 vec![
24332 (0, Breakpoint::new_standard()),
24333 (2, Breakpoint::new_standard()),
24334 (3, Breakpoint::new_standard()),
24335 ],
24336 );
24337
24338 editor.update_in(cx, |editor, window, cx| {
24339 editor.move_to_beginning(&MoveToBeginning, window, cx);
24340 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24341 editor.move_to_end(&MoveToEnd, window, cx);
24342 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24343 // Disabling a breakpoint that doesn't exist should do nothing
24344 editor.move_up(&MoveUp, window, cx);
24345 editor.move_up(&MoveUp, window, cx);
24346 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24347 });
24348
24349 let breakpoints = editor.update(cx, |editor, cx| {
24350 editor
24351 .breakpoint_store()
24352 .as_ref()
24353 .unwrap()
24354 .read(cx)
24355 .all_source_breakpoints(cx)
24356 });
24357
24358 let disable_breakpoint = {
24359 let mut bp = Breakpoint::new_standard();
24360 bp.state = BreakpointState::Disabled;
24361 bp
24362 };
24363
24364 assert_eq!(1, breakpoints.len());
24365 assert_breakpoint(
24366 &breakpoints,
24367 &abs_path,
24368 vec![
24369 (0, disable_breakpoint.clone()),
24370 (2, Breakpoint::new_standard()),
24371 (3, disable_breakpoint.clone()),
24372 ],
24373 );
24374
24375 editor.update_in(cx, |editor, window, cx| {
24376 editor.move_to_beginning(&MoveToBeginning, window, cx);
24377 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24378 editor.move_to_end(&MoveToEnd, window, cx);
24379 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24380 editor.move_up(&MoveUp, window, cx);
24381 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24382 });
24383
24384 let breakpoints = editor.update(cx, |editor, cx| {
24385 editor
24386 .breakpoint_store()
24387 .as_ref()
24388 .unwrap()
24389 .read(cx)
24390 .all_source_breakpoints(cx)
24391 });
24392
24393 assert_eq!(1, breakpoints.len());
24394 assert_breakpoint(
24395 &breakpoints,
24396 &abs_path,
24397 vec![
24398 (0, Breakpoint::new_standard()),
24399 (2, disable_breakpoint),
24400 (3, Breakpoint::new_standard()),
24401 ],
24402 );
24403}
24404
24405#[gpui::test]
24406async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
24407 init_test(cx, |_| {});
24408 let capabilities = lsp::ServerCapabilities {
24409 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
24410 prepare_provider: Some(true),
24411 work_done_progress_options: Default::default(),
24412 })),
24413 ..Default::default()
24414 };
24415 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24416
24417 cx.set_state(indoc! {"
24418 struct Fˇoo {}
24419 "});
24420
24421 cx.update_editor(|editor, _, cx| {
24422 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24423 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24424 editor.highlight_background::<DocumentHighlightRead>(
24425 &[highlight_range],
24426 |_, theme| theme.colors().editor_document_highlight_read_background,
24427 cx,
24428 );
24429 });
24430
24431 let mut prepare_rename_handler = cx
24432 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
24433 move |_, _, _| async move {
24434 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
24435 start: lsp::Position {
24436 line: 0,
24437 character: 7,
24438 },
24439 end: lsp::Position {
24440 line: 0,
24441 character: 10,
24442 },
24443 })))
24444 },
24445 );
24446 let prepare_rename_task = cx
24447 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24448 .expect("Prepare rename was not started");
24449 prepare_rename_handler.next().await.unwrap();
24450 prepare_rename_task.await.expect("Prepare rename failed");
24451
24452 let mut rename_handler =
24453 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24454 let edit = lsp::TextEdit {
24455 range: lsp::Range {
24456 start: lsp::Position {
24457 line: 0,
24458 character: 7,
24459 },
24460 end: lsp::Position {
24461 line: 0,
24462 character: 10,
24463 },
24464 },
24465 new_text: "FooRenamed".to_string(),
24466 };
24467 Ok(Some(lsp::WorkspaceEdit::new(
24468 // Specify the same edit twice
24469 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
24470 )))
24471 });
24472 let rename_task = cx
24473 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24474 .expect("Confirm rename was not started");
24475 rename_handler.next().await.unwrap();
24476 rename_task.await.expect("Confirm rename failed");
24477 cx.run_until_parked();
24478
24479 // Despite two edits, only one is actually applied as those are identical
24480 cx.assert_editor_state(indoc! {"
24481 struct FooRenamedˇ {}
24482 "});
24483}
24484
24485#[gpui::test]
24486async fn test_rename_without_prepare(cx: &mut TestAppContext) {
24487 init_test(cx, |_| {});
24488 // These capabilities indicate that the server does not support prepare rename.
24489 let capabilities = lsp::ServerCapabilities {
24490 rename_provider: Some(lsp::OneOf::Left(true)),
24491 ..Default::default()
24492 };
24493 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24494
24495 cx.set_state(indoc! {"
24496 struct Fˇoo {}
24497 "});
24498
24499 cx.update_editor(|editor, _window, cx| {
24500 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24501 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24502 editor.highlight_background::<DocumentHighlightRead>(
24503 &[highlight_range],
24504 |_, theme| theme.colors().editor_document_highlight_read_background,
24505 cx,
24506 );
24507 });
24508
24509 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24510 .expect("Prepare rename was not started")
24511 .await
24512 .expect("Prepare rename failed");
24513
24514 let mut rename_handler =
24515 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24516 let edit = lsp::TextEdit {
24517 range: lsp::Range {
24518 start: lsp::Position {
24519 line: 0,
24520 character: 7,
24521 },
24522 end: lsp::Position {
24523 line: 0,
24524 character: 10,
24525 },
24526 },
24527 new_text: "FooRenamed".to_string(),
24528 };
24529 Ok(Some(lsp::WorkspaceEdit::new(
24530 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
24531 )))
24532 });
24533 let rename_task = cx
24534 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
24535 .expect("Confirm rename was not started");
24536 rename_handler.next().await.unwrap();
24537 rename_task.await.expect("Confirm rename failed");
24538 cx.run_until_parked();
24539
24540 // Correct range is renamed, as `surrounding_word` is used to find it.
24541 cx.assert_editor_state(indoc! {"
24542 struct FooRenamedˇ {}
24543 "});
24544}
24545
24546#[gpui::test]
24547async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
24548 init_test(cx, |_| {});
24549 let mut cx = EditorTestContext::new(cx).await;
24550
24551 let language = Arc::new(
24552 Language::new(
24553 LanguageConfig::default(),
24554 Some(tree_sitter_html::LANGUAGE.into()),
24555 )
24556 .with_brackets_query(
24557 r#"
24558 ("<" @open "/>" @close)
24559 ("</" @open ">" @close)
24560 ("<" @open ">" @close)
24561 ("\"" @open "\"" @close)
24562 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
24563 "#,
24564 )
24565 .unwrap(),
24566 );
24567 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24568
24569 cx.set_state(indoc! {"
24570 <span>ˇ</span>
24571 "});
24572 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24573 cx.assert_editor_state(indoc! {"
24574 <span>
24575 ˇ
24576 </span>
24577 "});
24578
24579 cx.set_state(indoc! {"
24580 <span><span></span>ˇ</span>
24581 "});
24582 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24583 cx.assert_editor_state(indoc! {"
24584 <span><span></span>
24585 ˇ</span>
24586 "});
24587
24588 cx.set_state(indoc! {"
24589 <span>ˇ
24590 </span>
24591 "});
24592 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
24593 cx.assert_editor_state(indoc! {"
24594 <span>
24595 ˇ
24596 </span>
24597 "});
24598}
24599
24600#[gpui::test(iterations = 10)]
24601async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
24602 init_test(cx, |_| {});
24603
24604 let fs = FakeFs::new(cx.executor());
24605 fs.insert_tree(
24606 path!("/dir"),
24607 json!({
24608 "a.ts": "a",
24609 }),
24610 )
24611 .await;
24612
24613 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
24614 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24615 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24616
24617 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24618 language_registry.add(Arc::new(Language::new(
24619 LanguageConfig {
24620 name: "TypeScript".into(),
24621 matcher: LanguageMatcher {
24622 path_suffixes: vec!["ts".to_string()],
24623 ..Default::default()
24624 },
24625 ..Default::default()
24626 },
24627 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
24628 )));
24629 let mut fake_language_servers = language_registry.register_fake_lsp(
24630 "TypeScript",
24631 FakeLspAdapter {
24632 capabilities: lsp::ServerCapabilities {
24633 code_lens_provider: Some(lsp::CodeLensOptions {
24634 resolve_provider: Some(true),
24635 }),
24636 execute_command_provider: Some(lsp::ExecuteCommandOptions {
24637 commands: vec!["_the/command".to_string()],
24638 ..lsp::ExecuteCommandOptions::default()
24639 }),
24640 ..lsp::ServerCapabilities::default()
24641 },
24642 ..FakeLspAdapter::default()
24643 },
24644 );
24645
24646 let editor = workspace
24647 .update(cx, |workspace, window, cx| {
24648 workspace.open_abs_path(
24649 PathBuf::from(path!("/dir/a.ts")),
24650 OpenOptions::default(),
24651 window,
24652 cx,
24653 )
24654 })
24655 .unwrap()
24656 .await
24657 .unwrap()
24658 .downcast::<Editor>()
24659 .unwrap();
24660 cx.executor().run_until_parked();
24661
24662 let fake_server = fake_language_servers.next().await.unwrap();
24663
24664 let buffer = editor.update(cx, |editor, cx| {
24665 editor
24666 .buffer()
24667 .read(cx)
24668 .as_singleton()
24669 .expect("have opened a single file by path")
24670 });
24671
24672 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
24673 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
24674 drop(buffer_snapshot);
24675 let actions = cx
24676 .update_window(*workspace, |_, window, cx| {
24677 project.code_actions(&buffer, anchor..anchor, window, cx)
24678 })
24679 .unwrap();
24680
24681 fake_server
24682 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24683 Ok(Some(vec![
24684 lsp::CodeLens {
24685 range: lsp::Range::default(),
24686 command: Some(lsp::Command {
24687 title: "Code lens command".to_owned(),
24688 command: "_the/command".to_owned(),
24689 arguments: None,
24690 }),
24691 data: None,
24692 },
24693 lsp::CodeLens {
24694 range: lsp::Range::default(),
24695 command: Some(lsp::Command {
24696 title: "Command not in capabilities".to_owned(),
24697 command: "not in capabilities".to_owned(),
24698 arguments: None,
24699 }),
24700 data: None,
24701 },
24702 lsp::CodeLens {
24703 range: lsp::Range {
24704 start: lsp::Position {
24705 line: 1,
24706 character: 1,
24707 },
24708 end: lsp::Position {
24709 line: 1,
24710 character: 1,
24711 },
24712 },
24713 command: Some(lsp::Command {
24714 title: "Command not in range".to_owned(),
24715 command: "_the/command".to_owned(),
24716 arguments: None,
24717 }),
24718 data: None,
24719 },
24720 ]))
24721 })
24722 .next()
24723 .await;
24724
24725 let actions = actions.await.unwrap();
24726 assert_eq!(
24727 actions.len(),
24728 1,
24729 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
24730 );
24731 let action = actions[0].clone();
24732 let apply = project.update(cx, |project, cx| {
24733 project.apply_code_action(buffer.clone(), action, true, cx)
24734 });
24735
24736 // Resolving the code action does not populate its edits. In absence of
24737 // edits, we must execute the given command.
24738 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
24739 |mut lens, _| async move {
24740 let lens_command = lens.command.as_mut().expect("should have a command");
24741 assert_eq!(lens_command.title, "Code lens command");
24742 lens_command.arguments = Some(vec![json!("the-argument")]);
24743 Ok(lens)
24744 },
24745 );
24746
24747 // While executing the command, the language server sends the editor
24748 // a `workspaceEdit` request.
24749 fake_server
24750 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
24751 let fake = fake_server.clone();
24752 move |params, _| {
24753 assert_eq!(params.command, "_the/command");
24754 let fake = fake.clone();
24755 async move {
24756 fake.server
24757 .request::<lsp::request::ApplyWorkspaceEdit>(
24758 lsp::ApplyWorkspaceEditParams {
24759 label: None,
24760 edit: lsp::WorkspaceEdit {
24761 changes: Some(
24762 [(
24763 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
24764 vec![lsp::TextEdit {
24765 range: lsp::Range::new(
24766 lsp::Position::new(0, 0),
24767 lsp::Position::new(0, 0),
24768 ),
24769 new_text: "X".into(),
24770 }],
24771 )]
24772 .into_iter()
24773 .collect(),
24774 ),
24775 ..lsp::WorkspaceEdit::default()
24776 },
24777 },
24778 )
24779 .await
24780 .into_response()
24781 .unwrap();
24782 Ok(Some(json!(null)))
24783 }
24784 }
24785 })
24786 .next()
24787 .await;
24788
24789 // Applying the code lens command returns a project transaction containing the edits
24790 // sent by the language server in its `workspaceEdit` request.
24791 let transaction = apply.await.unwrap();
24792 assert!(transaction.0.contains_key(&buffer));
24793 buffer.update(cx, |buffer, cx| {
24794 assert_eq!(buffer.text(), "Xa");
24795 buffer.undo(cx);
24796 assert_eq!(buffer.text(), "a");
24797 });
24798
24799 let actions_after_edits = cx
24800 .update_window(*workspace, |_, window, cx| {
24801 project.code_actions(&buffer, anchor..anchor, window, cx)
24802 })
24803 .unwrap()
24804 .await
24805 .unwrap();
24806 assert_eq!(
24807 actions, actions_after_edits,
24808 "For the same selection, same code lens actions should be returned"
24809 );
24810
24811 let _responses =
24812 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
24813 panic!("No more code lens requests are expected");
24814 });
24815 editor.update_in(cx, |editor, window, cx| {
24816 editor.select_all(&SelectAll, window, cx);
24817 });
24818 cx.executor().run_until_parked();
24819 let new_actions = cx
24820 .update_window(*workspace, |_, window, cx| {
24821 project.code_actions(&buffer, anchor..anchor, window, cx)
24822 })
24823 .unwrap()
24824 .await
24825 .unwrap();
24826 assert_eq!(
24827 actions, new_actions,
24828 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
24829 );
24830}
24831
24832#[gpui::test]
24833async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
24834 init_test(cx, |_| {});
24835
24836 let fs = FakeFs::new(cx.executor());
24837 let main_text = r#"fn main() {
24838println!("1");
24839println!("2");
24840println!("3");
24841println!("4");
24842println!("5");
24843}"#;
24844 let lib_text = "mod foo {}";
24845 fs.insert_tree(
24846 path!("/a"),
24847 json!({
24848 "lib.rs": lib_text,
24849 "main.rs": main_text,
24850 }),
24851 )
24852 .await;
24853
24854 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24855 let (workspace, cx) =
24856 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24857 let worktree_id = workspace.update(cx, |workspace, cx| {
24858 workspace.project().update(cx, |project, cx| {
24859 project.worktrees(cx).next().unwrap().read(cx).id()
24860 })
24861 });
24862
24863 let expected_ranges = vec![
24864 Point::new(0, 0)..Point::new(0, 0),
24865 Point::new(1, 0)..Point::new(1, 1),
24866 Point::new(2, 0)..Point::new(2, 2),
24867 Point::new(3, 0)..Point::new(3, 3),
24868 ];
24869
24870 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24871 let editor_1 = workspace
24872 .update_in(cx, |workspace, window, cx| {
24873 workspace.open_path(
24874 (worktree_id, rel_path("main.rs")),
24875 Some(pane_1.downgrade()),
24876 true,
24877 window,
24878 cx,
24879 )
24880 })
24881 .unwrap()
24882 .await
24883 .downcast::<Editor>()
24884 .unwrap();
24885 pane_1.update(cx, |pane, cx| {
24886 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24887 open_editor.update(cx, |editor, cx| {
24888 assert_eq!(
24889 editor.display_text(cx),
24890 main_text,
24891 "Original main.rs text on initial open",
24892 );
24893 assert_eq!(
24894 editor
24895 .selections
24896 .all::<Point>(&editor.display_snapshot(cx))
24897 .into_iter()
24898 .map(|s| s.range())
24899 .collect::<Vec<_>>(),
24900 vec![Point::zero()..Point::zero()],
24901 "Default selections on initial open",
24902 );
24903 })
24904 });
24905 editor_1.update_in(cx, |editor, window, cx| {
24906 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
24907 s.select_ranges(expected_ranges.clone());
24908 });
24909 });
24910
24911 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
24912 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
24913 });
24914 let editor_2 = workspace
24915 .update_in(cx, |workspace, window, cx| {
24916 workspace.open_path(
24917 (worktree_id, rel_path("main.rs")),
24918 Some(pane_2.downgrade()),
24919 true,
24920 window,
24921 cx,
24922 )
24923 })
24924 .unwrap()
24925 .await
24926 .downcast::<Editor>()
24927 .unwrap();
24928 pane_2.update(cx, |pane, cx| {
24929 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24930 open_editor.update(cx, |editor, cx| {
24931 assert_eq!(
24932 editor.display_text(cx),
24933 main_text,
24934 "Original main.rs text on initial open in another panel",
24935 );
24936 assert_eq!(
24937 editor
24938 .selections
24939 .all::<Point>(&editor.display_snapshot(cx))
24940 .into_iter()
24941 .map(|s| s.range())
24942 .collect::<Vec<_>>(),
24943 vec![Point::zero()..Point::zero()],
24944 "Default selections on initial open in another panel",
24945 );
24946 })
24947 });
24948
24949 editor_2.update_in(cx, |editor, window, cx| {
24950 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
24951 });
24952
24953 let _other_editor_1 = workspace
24954 .update_in(cx, |workspace, window, cx| {
24955 workspace.open_path(
24956 (worktree_id, rel_path("lib.rs")),
24957 Some(pane_1.downgrade()),
24958 true,
24959 window,
24960 cx,
24961 )
24962 })
24963 .unwrap()
24964 .await
24965 .downcast::<Editor>()
24966 .unwrap();
24967 pane_1
24968 .update_in(cx, |pane, window, cx| {
24969 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24970 })
24971 .await
24972 .unwrap();
24973 drop(editor_1);
24974 pane_1.update(cx, |pane, cx| {
24975 pane.active_item()
24976 .unwrap()
24977 .downcast::<Editor>()
24978 .unwrap()
24979 .update(cx, |editor, cx| {
24980 assert_eq!(
24981 editor.display_text(cx),
24982 lib_text,
24983 "Other file should be open and active",
24984 );
24985 });
24986 assert_eq!(pane.items().count(), 1, "No other editors should be open");
24987 });
24988
24989 let _other_editor_2 = workspace
24990 .update_in(cx, |workspace, window, cx| {
24991 workspace.open_path(
24992 (worktree_id, rel_path("lib.rs")),
24993 Some(pane_2.downgrade()),
24994 true,
24995 window,
24996 cx,
24997 )
24998 })
24999 .unwrap()
25000 .await
25001 .downcast::<Editor>()
25002 .unwrap();
25003 pane_2
25004 .update_in(cx, |pane, window, cx| {
25005 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
25006 })
25007 .await
25008 .unwrap();
25009 drop(editor_2);
25010 pane_2.update(cx, |pane, cx| {
25011 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25012 open_editor.update(cx, |editor, cx| {
25013 assert_eq!(
25014 editor.display_text(cx),
25015 lib_text,
25016 "Other file should be open and active in another panel too",
25017 );
25018 });
25019 assert_eq!(
25020 pane.items().count(),
25021 1,
25022 "No other editors should be open in another pane",
25023 );
25024 });
25025
25026 let _editor_1_reopened = workspace
25027 .update_in(cx, |workspace, window, cx| {
25028 workspace.open_path(
25029 (worktree_id, rel_path("main.rs")),
25030 Some(pane_1.downgrade()),
25031 true,
25032 window,
25033 cx,
25034 )
25035 })
25036 .unwrap()
25037 .await
25038 .downcast::<Editor>()
25039 .unwrap();
25040 let _editor_2_reopened = workspace
25041 .update_in(cx, |workspace, window, cx| {
25042 workspace.open_path(
25043 (worktree_id, rel_path("main.rs")),
25044 Some(pane_2.downgrade()),
25045 true,
25046 window,
25047 cx,
25048 )
25049 })
25050 .unwrap()
25051 .await
25052 .downcast::<Editor>()
25053 .unwrap();
25054 pane_1.update(cx, |pane, cx| {
25055 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25056 open_editor.update(cx, |editor, cx| {
25057 assert_eq!(
25058 editor.display_text(cx),
25059 main_text,
25060 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
25061 );
25062 assert_eq!(
25063 editor
25064 .selections
25065 .all::<Point>(&editor.display_snapshot(cx))
25066 .into_iter()
25067 .map(|s| s.range())
25068 .collect::<Vec<_>>(),
25069 expected_ranges,
25070 "Previous editor in the 1st panel had selections and should get them restored on reopen",
25071 );
25072 })
25073 });
25074 pane_2.update(cx, |pane, cx| {
25075 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25076 open_editor.update(cx, |editor, cx| {
25077 assert_eq!(
25078 editor.display_text(cx),
25079 r#"fn main() {
25080⋯rintln!("1");
25081⋯intln!("2");
25082⋯ntln!("3");
25083println!("4");
25084println!("5");
25085}"#,
25086 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
25087 );
25088 assert_eq!(
25089 editor
25090 .selections
25091 .all::<Point>(&editor.display_snapshot(cx))
25092 .into_iter()
25093 .map(|s| s.range())
25094 .collect::<Vec<_>>(),
25095 vec![Point::zero()..Point::zero()],
25096 "Previous editor in the 2nd pane had no selections changed hence should restore none",
25097 );
25098 })
25099 });
25100}
25101
25102#[gpui::test]
25103async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
25104 init_test(cx, |_| {});
25105
25106 let fs = FakeFs::new(cx.executor());
25107 let main_text = r#"fn main() {
25108println!("1");
25109println!("2");
25110println!("3");
25111println!("4");
25112println!("5");
25113}"#;
25114 let lib_text = "mod foo {}";
25115 fs.insert_tree(
25116 path!("/a"),
25117 json!({
25118 "lib.rs": lib_text,
25119 "main.rs": main_text,
25120 }),
25121 )
25122 .await;
25123
25124 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25125 let (workspace, cx) =
25126 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25127 let worktree_id = workspace.update(cx, |workspace, cx| {
25128 workspace.project().update(cx, |project, cx| {
25129 project.worktrees(cx).next().unwrap().read(cx).id()
25130 })
25131 });
25132
25133 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25134 let editor = workspace
25135 .update_in(cx, |workspace, window, cx| {
25136 workspace.open_path(
25137 (worktree_id, rel_path("main.rs")),
25138 Some(pane.downgrade()),
25139 true,
25140 window,
25141 cx,
25142 )
25143 })
25144 .unwrap()
25145 .await
25146 .downcast::<Editor>()
25147 .unwrap();
25148 pane.update(cx, |pane, cx| {
25149 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25150 open_editor.update(cx, |editor, cx| {
25151 assert_eq!(
25152 editor.display_text(cx),
25153 main_text,
25154 "Original main.rs text on initial open",
25155 );
25156 })
25157 });
25158 editor.update_in(cx, |editor, window, cx| {
25159 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
25160 });
25161
25162 cx.update_global(|store: &mut SettingsStore, cx| {
25163 store.update_user_settings(cx, |s| {
25164 s.workspace.restore_on_file_reopen = Some(false);
25165 });
25166 });
25167 editor.update_in(cx, |editor, window, cx| {
25168 editor.fold_ranges(
25169 vec![
25170 Point::new(1, 0)..Point::new(1, 1),
25171 Point::new(2, 0)..Point::new(2, 2),
25172 Point::new(3, 0)..Point::new(3, 3),
25173 ],
25174 false,
25175 window,
25176 cx,
25177 );
25178 });
25179 pane.update_in(cx, |pane, window, cx| {
25180 pane.close_all_items(&CloseAllItems::default(), window, cx)
25181 })
25182 .await
25183 .unwrap();
25184 pane.update(cx, |pane, _| {
25185 assert!(pane.active_item().is_none());
25186 });
25187 cx.update_global(|store: &mut SettingsStore, cx| {
25188 store.update_user_settings(cx, |s| {
25189 s.workspace.restore_on_file_reopen = Some(true);
25190 });
25191 });
25192
25193 let _editor_reopened = workspace
25194 .update_in(cx, |workspace, window, cx| {
25195 workspace.open_path(
25196 (worktree_id, rel_path("main.rs")),
25197 Some(pane.downgrade()),
25198 true,
25199 window,
25200 cx,
25201 )
25202 })
25203 .unwrap()
25204 .await
25205 .downcast::<Editor>()
25206 .unwrap();
25207 pane.update(cx, |pane, cx| {
25208 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25209 open_editor.update(cx, |editor, cx| {
25210 assert_eq!(
25211 editor.display_text(cx),
25212 main_text,
25213 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
25214 );
25215 })
25216 });
25217}
25218
25219#[gpui::test]
25220async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
25221 struct EmptyModalView {
25222 focus_handle: gpui::FocusHandle,
25223 }
25224 impl EventEmitter<DismissEvent> for EmptyModalView {}
25225 impl Render for EmptyModalView {
25226 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
25227 div()
25228 }
25229 }
25230 impl Focusable for EmptyModalView {
25231 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
25232 self.focus_handle.clone()
25233 }
25234 }
25235 impl workspace::ModalView for EmptyModalView {}
25236 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
25237 EmptyModalView {
25238 focus_handle: cx.focus_handle(),
25239 }
25240 }
25241
25242 init_test(cx, |_| {});
25243
25244 let fs = FakeFs::new(cx.executor());
25245 let project = Project::test(fs, [], cx).await;
25246 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25247 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
25248 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
25249 let editor = cx.new_window_entity(|window, cx| {
25250 Editor::new(
25251 EditorMode::full(),
25252 buffer,
25253 Some(project.clone()),
25254 window,
25255 cx,
25256 )
25257 });
25258 workspace
25259 .update(cx, |workspace, window, cx| {
25260 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
25261 })
25262 .unwrap();
25263 editor.update_in(cx, |editor, window, cx| {
25264 editor.open_context_menu(&OpenContextMenu, window, cx);
25265 assert!(editor.mouse_context_menu.is_some());
25266 });
25267 workspace
25268 .update(cx, |workspace, window, cx| {
25269 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
25270 })
25271 .unwrap();
25272 cx.read(|cx| {
25273 assert!(editor.read(cx).mouse_context_menu.is_none());
25274 });
25275}
25276
25277fn set_linked_edit_ranges(
25278 opening: (Point, Point),
25279 closing: (Point, Point),
25280 editor: &mut Editor,
25281 cx: &mut Context<Editor>,
25282) {
25283 let Some((buffer, _)) = editor
25284 .buffer
25285 .read(cx)
25286 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
25287 else {
25288 panic!("Failed to get buffer for selection position");
25289 };
25290 let buffer = buffer.read(cx);
25291 let buffer_id = buffer.remote_id();
25292 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
25293 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
25294 let mut linked_ranges = HashMap::default();
25295 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
25296 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
25297}
25298
25299#[gpui::test]
25300async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
25301 init_test(cx, |_| {});
25302
25303 let fs = FakeFs::new(cx.executor());
25304 fs.insert_file(path!("/file.html"), Default::default())
25305 .await;
25306
25307 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
25308
25309 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25310 let html_language = Arc::new(Language::new(
25311 LanguageConfig {
25312 name: "HTML".into(),
25313 matcher: LanguageMatcher {
25314 path_suffixes: vec!["html".to_string()],
25315 ..LanguageMatcher::default()
25316 },
25317 brackets: BracketPairConfig {
25318 pairs: vec![BracketPair {
25319 start: "<".into(),
25320 end: ">".into(),
25321 close: true,
25322 ..Default::default()
25323 }],
25324 ..Default::default()
25325 },
25326 ..Default::default()
25327 },
25328 Some(tree_sitter_html::LANGUAGE.into()),
25329 ));
25330 language_registry.add(html_language);
25331 let mut fake_servers = language_registry.register_fake_lsp(
25332 "HTML",
25333 FakeLspAdapter {
25334 capabilities: lsp::ServerCapabilities {
25335 completion_provider: Some(lsp::CompletionOptions {
25336 resolve_provider: Some(true),
25337 ..Default::default()
25338 }),
25339 ..Default::default()
25340 },
25341 ..Default::default()
25342 },
25343 );
25344
25345 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25346 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25347
25348 let worktree_id = workspace
25349 .update(cx, |workspace, _window, cx| {
25350 workspace.project().update(cx, |project, cx| {
25351 project.worktrees(cx).next().unwrap().read(cx).id()
25352 })
25353 })
25354 .unwrap();
25355 project
25356 .update(cx, |project, cx| {
25357 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
25358 })
25359 .await
25360 .unwrap();
25361 let editor = workspace
25362 .update(cx, |workspace, window, cx| {
25363 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
25364 })
25365 .unwrap()
25366 .await
25367 .unwrap()
25368 .downcast::<Editor>()
25369 .unwrap();
25370
25371 let fake_server = fake_servers.next().await.unwrap();
25372 editor.update_in(cx, |editor, window, cx| {
25373 editor.set_text("<ad></ad>", window, cx);
25374 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
25375 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
25376 });
25377 set_linked_edit_ranges(
25378 (Point::new(0, 1), Point::new(0, 3)),
25379 (Point::new(0, 6), Point::new(0, 8)),
25380 editor,
25381 cx,
25382 );
25383 });
25384 let mut completion_handle =
25385 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
25386 Ok(Some(lsp::CompletionResponse::Array(vec![
25387 lsp::CompletionItem {
25388 label: "head".to_string(),
25389 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25390 lsp::InsertReplaceEdit {
25391 new_text: "head".to_string(),
25392 insert: lsp::Range::new(
25393 lsp::Position::new(0, 1),
25394 lsp::Position::new(0, 3),
25395 ),
25396 replace: lsp::Range::new(
25397 lsp::Position::new(0, 1),
25398 lsp::Position::new(0, 3),
25399 ),
25400 },
25401 )),
25402 ..Default::default()
25403 },
25404 ])))
25405 });
25406 editor.update_in(cx, |editor, window, cx| {
25407 editor.show_completions(&ShowCompletions, window, cx);
25408 });
25409 cx.run_until_parked();
25410 completion_handle.next().await.unwrap();
25411 editor.update(cx, |editor, _| {
25412 assert!(
25413 editor.context_menu_visible(),
25414 "Completion menu should be visible"
25415 );
25416 });
25417 editor.update_in(cx, |editor, window, cx| {
25418 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
25419 });
25420 cx.executor().run_until_parked();
25421 editor.update(cx, |editor, cx| {
25422 assert_eq!(editor.text(cx), "<head></head>");
25423 });
25424}
25425
25426#[gpui::test]
25427async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
25428 init_test(cx, |_| {});
25429
25430 let mut cx = EditorTestContext::new(cx).await;
25431 let language = Arc::new(Language::new(
25432 LanguageConfig {
25433 name: "TSX".into(),
25434 matcher: LanguageMatcher {
25435 path_suffixes: vec!["tsx".to_string()],
25436 ..LanguageMatcher::default()
25437 },
25438 brackets: BracketPairConfig {
25439 pairs: vec![BracketPair {
25440 start: "<".into(),
25441 end: ">".into(),
25442 close: true,
25443 ..Default::default()
25444 }],
25445 ..Default::default()
25446 },
25447 linked_edit_characters: HashSet::from_iter(['.']),
25448 ..Default::default()
25449 },
25450 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
25451 ));
25452 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25453
25454 // Test typing > does not extend linked pair
25455 cx.set_state("<divˇ<div></div>");
25456 cx.update_editor(|editor, _, cx| {
25457 set_linked_edit_ranges(
25458 (Point::new(0, 1), Point::new(0, 4)),
25459 (Point::new(0, 11), Point::new(0, 14)),
25460 editor,
25461 cx,
25462 );
25463 });
25464 cx.update_editor(|editor, window, cx| {
25465 editor.handle_input(">", window, cx);
25466 });
25467 cx.assert_editor_state("<div>ˇ<div></div>");
25468
25469 // Test typing . do extend linked pair
25470 cx.set_state("<Animatedˇ></Animated>");
25471 cx.update_editor(|editor, _, cx| {
25472 set_linked_edit_ranges(
25473 (Point::new(0, 1), Point::new(0, 9)),
25474 (Point::new(0, 12), Point::new(0, 20)),
25475 editor,
25476 cx,
25477 );
25478 });
25479 cx.update_editor(|editor, window, cx| {
25480 editor.handle_input(".", window, cx);
25481 });
25482 cx.assert_editor_state("<Animated.ˇ></Animated.>");
25483 cx.update_editor(|editor, _, cx| {
25484 set_linked_edit_ranges(
25485 (Point::new(0, 1), Point::new(0, 10)),
25486 (Point::new(0, 13), Point::new(0, 21)),
25487 editor,
25488 cx,
25489 );
25490 });
25491 cx.update_editor(|editor, window, cx| {
25492 editor.handle_input("V", window, cx);
25493 });
25494 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
25495}
25496
25497#[gpui::test]
25498async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
25499 init_test(cx, |_| {});
25500
25501 let fs = FakeFs::new(cx.executor());
25502 fs.insert_tree(
25503 path!("/root"),
25504 json!({
25505 "a": {
25506 "main.rs": "fn main() {}",
25507 },
25508 "foo": {
25509 "bar": {
25510 "external_file.rs": "pub mod external {}",
25511 }
25512 }
25513 }),
25514 )
25515 .await;
25516
25517 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
25518 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25519 language_registry.add(rust_lang());
25520 let _fake_servers = language_registry.register_fake_lsp(
25521 "Rust",
25522 FakeLspAdapter {
25523 ..FakeLspAdapter::default()
25524 },
25525 );
25526 let (workspace, cx) =
25527 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25528 let worktree_id = workspace.update(cx, |workspace, cx| {
25529 workspace.project().update(cx, |project, cx| {
25530 project.worktrees(cx).next().unwrap().read(cx).id()
25531 })
25532 });
25533
25534 let assert_language_servers_count =
25535 |expected: usize, context: &str, cx: &mut VisualTestContext| {
25536 project.update(cx, |project, cx| {
25537 let current = project
25538 .lsp_store()
25539 .read(cx)
25540 .as_local()
25541 .unwrap()
25542 .language_servers
25543 .len();
25544 assert_eq!(expected, current, "{context}");
25545 });
25546 };
25547
25548 assert_language_servers_count(
25549 0,
25550 "No servers should be running before any file is open",
25551 cx,
25552 );
25553 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25554 let main_editor = workspace
25555 .update_in(cx, |workspace, window, cx| {
25556 workspace.open_path(
25557 (worktree_id, rel_path("main.rs")),
25558 Some(pane.downgrade()),
25559 true,
25560 window,
25561 cx,
25562 )
25563 })
25564 .unwrap()
25565 .await
25566 .downcast::<Editor>()
25567 .unwrap();
25568 pane.update(cx, |pane, cx| {
25569 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25570 open_editor.update(cx, |editor, cx| {
25571 assert_eq!(
25572 editor.display_text(cx),
25573 "fn main() {}",
25574 "Original main.rs text on initial open",
25575 );
25576 });
25577 assert_eq!(open_editor, main_editor);
25578 });
25579 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
25580
25581 let external_editor = workspace
25582 .update_in(cx, |workspace, window, cx| {
25583 workspace.open_abs_path(
25584 PathBuf::from("/root/foo/bar/external_file.rs"),
25585 OpenOptions::default(),
25586 window,
25587 cx,
25588 )
25589 })
25590 .await
25591 .expect("opening external file")
25592 .downcast::<Editor>()
25593 .expect("downcasted external file's open element to editor");
25594 pane.update(cx, |pane, cx| {
25595 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25596 open_editor.update(cx, |editor, cx| {
25597 assert_eq!(
25598 editor.display_text(cx),
25599 "pub mod external {}",
25600 "External file is open now",
25601 );
25602 });
25603 assert_eq!(open_editor, external_editor);
25604 });
25605 assert_language_servers_count(
25606 1,
25607 "Second, external, *.rs file should join the existing server",
25608 cx,
25609 );
25610
25611 pane.update_in(cx, |pane, window, cx| {
25612 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25613 })
25614 .await
25615 .unwrap();
25616 pane.update_in(cx, |pane, window, cx| {
25617 pane.navigate_backward(&Default::default(), window, cx);
25618 });
25619 cx.run_until_parked();
25620 pane.update(cx, |pane, cx| {
25621 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25622 open_editor.update(cx, |editor, cx| {
25623 assert_eq!(
25624 editor.display_text(cx),
25625 "pub mod external {}",
25626 "External file is open now",
25627 );
25628 });
25629 });
25630 assert_language_servers_count(
25631 1,
25632 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
25633 cx,
25634 );
25635
25636 cx.update(|_, cx| {
25637 workspace::reload(cx);
25638 });
25639 assert_language_servers_count(
25640 1,
25641 "After reloading the worktree with local and external files opened, only one project should be started",
25642 cx,
25643 );
25644}
25645
25646#[gpui::test]
25647async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
25648 init_test(cx, |_| {});
25649
25650 let mut cx = EditorTestContext::new(cx).await;
25651 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25652 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25653
25654 // test cursor move to start of each line on tab
25655 // for `if`, `elif`, `else`, `while`, `with` and `for`
25656 cx.set_state(indoc! {"
25657 def main():
25658 ˇ for item in items:
25659 ˇ while item.active:
25660 ˇ if item.value > 10:
25661 ˇ continue
25662 ˇ elif item.value < 0:
25663 ˇ break
25664 ˇ else:
25665 ˇ with item.context() as ctx:
25666 ˇ yield count
25667 ˇ else:
25668 ˇ log('while else')
25669 ˇ else:
25670 ˇ log('for else')
25671 "});
25672 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25673 cx.wait_for_autoindent_applied().await;
25674 cx.assert_editor_state(indoc! {"
25675 def main():
25676 ˇfor item in items:
25677 ˇwhile item.active:
25678 ˇif item.value > 10:
25679 ˇcontinue
25680 ˇelif item.value < 0:
25681 ˇbreak
25682 ˇelse:
25683 ˇwith item.context() as ctx:
25684 ˇyield count
25685 ˇelse:
25686 ˇlog('while else')
25687 ˇelse:
25688 ˇlog('for else')
25689 "});
25690 // test relative indent is preserved when tab
25691 // for `if`, `elif`, `else`, `while`, `with` and `for`
25692 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25693 cx.wait_for_autoindent_applied().await;
25694 cx.assert_editor_state(indoc! {"
25695 def main():
25696 ˇfor item in items:
25697 ˇwhile item.active:
25698 ˇif item.value > 10:
25699 ˇcontinue
25700 ˇelif item.value < 0:
25701 ˇbreak
25702 ˇelse:
25703 ˇwith item.context() as ctx:
25704 ˇyield count
25705 ˇelse:
25706 ˇlog('while else')
25707 ˇelse:
25708 ˇlog('for else')
25709 "});
25710
25711 // test cursor move to start of each line on tab
25712 // for `try`, `except`, `else`, `finally`, `match` and `def`
25713 cx.set_state(indoc! {"
25714 def main():
25715 ˇ try:
25716 ˇ fetch()
25717 ˇ except ValueError:
25718 ˇ handle_error()
25719 ˇ else:
25720 ˇ match value:
25721 ˇ case _:
25722 ˇ finally:
25723 ˇ def status():
25724 ˇ return 0
25725 "});
25726 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25727 cx.wait_for_autoindent_applied().await;
25728 cx.assert_editor_state(indoc! {"
25729 def main():
25730 ˇtry:
25731 ˇfetch()
25732 ˇexcept ValueError:
25733 ˇhandle_error()
25734 ˇelse:
25735 ˇmatch value:
25736 ˇcase _:
25737 ˇfinally:
25738 ˇdef status():
25739 ˇreturn 0
25740 "});
25741 // test relative indent is preserved when tab
25742 // for `try`, `except`, `else`, `finally`, `match` and `def`
25743 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25744 cx.wait_for_autoindent_applied().await;
25745 cx.assert_editor_state(indoc! {"
25746 def main():
25747 ˇtry:
25748 ˇfetch()
25749 ˇexcept ValueError:
25750 ˇhandle_error()
25751 ˇelse:
25752 ˇmatch value:
25753 ˇcase _:
25754 ˇfinally:
25755 ˇdef status():
25756 ˇreturn 0
25757 "});
25758}
25759
25760#[gpui::test]
25761async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
25762 init_test(cx, |_| {});
25763
25764 let mut cx = EditorTestContext::new(cx).await;
25765 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25766 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25767
25768 // test `else` auto outdents when typed inside `if` block
25769 cx.set_state(indoc! {"
25770 def main():
25771 if i == 2:
25772 return
25773 ˇ
25774 "});
25775 cx.update_editor(|editor, window, cx| {
25776 editor.handle_input("else:", window, cx);
25777 });
25778 cx.wait_for_autoindent_applied().await;
25779 cx.assert_editor_state(indoc! {"
25780 def main():
25781 if i == 2:
25782 return
25783 else:ˇ
25784 "});
25785
25786 // test `except` auto outdents when typed inside `try` block
25787 cx.set_state(indoc! {"
25788 def main():
25789 try:
25790 i = 2
25791 ˇ
25792 "});
25793 cx.update_editor(|editor, window, cx| {
25794 editor.handle_input("except:", window, cx);
25795 });
25796 cx.wait_for_autoindent_applied().await;
25797 cx.assert_editor_state(indoc! {"
25798 def main():
25799 try:
25800 i = 2
25801 except:ˇ
25802 "});
25803
25804 // test `else` auto outdents when typed inside `except` block
25805 cx.set_state(indoc! {"
25806 def main():
25807 try:
25808 i = 2
25809 except:
25810 j = 2
25811 ˇ
25812 "});
25813 cx.update_editor(|editor, window, cx| {
25814 editor.handle_input("else:", window, cx);
25815 });
25816 cx.wait_for_autoindent_applied().await;
25817 cx.assert_editor_state(indoc! {"
25818 def main():
25819 try:
25820 i = 2
25821 except:
25822 j = 2
25823 else:ˇ
25824 "});
25825
25826 // test `finally` auto outdents when typed inside `else` block
25827 cx.set_state(indoc! {"
25828 def main():
25829 try:
25830 i = 2
25831 except:
25832 j = 2
25833 else:
25834 k = 2
25835 ˇ
25836 "});
25837 cx.update_editor(|editor, window, cx| {
25838 editor.handle_input("finally:", window, cx);
25839 });
25840 cx.wait_for_autoindent_applied().await;
25841 cx.assert_editor_state(indoc! {"
25842 def main():
25843 try:
25844 i = 2
25845 except:
25846 j = 2
25847 else:
25848 k = 2
25849 finally:ˇ
25850 "});
25851
25852 // test `else` does not outdents when typed inside `except` block right after for block
25853 cx.set_state(indoc! {"
25854 def main():
25855 try:
25856 i = 2
25857 except:
25858 for i in range(n):
25859 pass
25860 ˇ
25861 "});
25862 cx.update_editor(|editor, window, cx| {
25863 editor.handle_input("else:", window, cx);
25864 });
25865 cx.wait_for_autoindent_applied().await;
25866 cx.assert_editor_state(indoc! {"
25867 def main():
25868 try:
25869 i = 2
25870 except:
25871 for i in range(n):
25872 pass
25873 else:ˇ
25874 "});
25875
25876 // test `finally` auto outdents when typed inside `else` block right after for block
25877 cx.set_state(indoc! {"
25878 def main():
25879 try:
25880 i = 2
25881 except:
25882 j = 2
25883 else:
25884 for i in range(n):
25885 pass
25886 ˇ
25887 "});
25888 cx.update_editor(|editor, window, cx| {
25889 editor.handle_input("finally:", window, cx);
25890 });
25891 cx.wait_for_autoindent_applied().await;
25892 cx.assert_editor_state(indoc! {"
25893 def main():
25894 try:
25895 i = 2
25896 except:
25897 j = 2
25898 else:
25899 for i in range(n):
25900 pass
25901 finally:ˇ
25902 "});
25903
25904 // test `except` outdents to inner "try" block
25905 cx.set_state(indoc! {"
25906 def main():
25907 try:
25908 i = 2
25909 if i == 2:
25910 try:
25911 i = 3
25912 ˇ
25913 "});
25914 cx.update_editor(|editor, window, cx| {
25915 editor.handle_input("except:", window, cx);
25916 });
25917 cx.wait_for_autoindent_applied().await;
25918 cx.assert_editor_state(indoc! {"
25919 def main():
25920 try:
25921 i = 2
25922 if i == 2:
25923 try:
25924 i = 3
25925 except:ˇ
25926 "});
25927
25928 // test `except` outdents to outer "try" block
25929 cx.set_state(indoc! {"
25930 def main():
25931 try:
25932 i = 2
25933 if i == 2:
25934 try:
25935 i = 3
25936 ˇ
25937 "});
25938 cx.update_editor(|editor, window, cx| {
25939 editor.handle_input("except:", window, cx);
25940 });
25941 cx.wait_for_autoindent_applied().await;
25942 cx.assert_editor_state(indoc! {"
25943 def main():
25944 try:
25945 i = 2
25946 if i == 2:
25947 try:
25948 i = 3
25949 except:ˇ
25950 "});
25951
25952 // test `else` stays at correct indent when typed after `for` block
25953 cx.set_state(indoc! {"
25954 def main():
25955 for i in range(10):
25956 if i == 3:
25957 break
25958 ˇ
25959 "});
25960 cx.update_editor(|editor, window, cx| {
25961 editor.handle_input("else:", window, cx);
25962 });
25963 cx.wait_for_autoindent_applied().await;
25964 cx.assert_editor_state(indoc! {"
25965 def main():
25966 for i in range(10):
25967 if i == 3:
25968 break
25969 else:ˇ
25970 "});
25971
25972 // test does not outdent on typing after line with square brackets
25973 cx.set_state(indoc! {"
25974 def f() -> list[str]:
25975 ˇ
25976 "});
25977 cx.update_editor(|editor, window, cx| {
25978 editor.handle_input("a", window, cx);
25979 });
25980 cx.wait_for_autoindent_applied().await;
25981 cx.assert_editor_state(indoc! {"
25982 def f() -> list[str]:
25983 aˇ
25984 "});
25985
25986 // test does not outdent on typing : after case keyword
25987 cx.set_state(indoc! {"
25988 match 1:
25989 caseˇ
25990 "});
25991 cx.update_editor(|editor, window, cx| {
25992 editor.handle_input(":", window, cx);
25993 });
25994 cx.wait_for_autoindent_applied().await;
25995 cx.assert_editor_state(indoc! {"
25996 match 1:
25997 case:ˇ
25998 "});
25999}
26000
26001#[gpui::test]
26002async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
26003 init_test(cx, |_| {});
26004 update_test_language_settings(cx, |settings| {
26005 settings.defaults.extend_comment_on_newline = Some(false);
26006 });
26007 let mut cx = EditorTestContext::new(cx).await;
26008 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
26009 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26010
26011 // test correct indent after newline on comment
26012 cx.set_state(indoc! {"
26013 # COMMENT:ˇ
26014 "});
26015 cx.update_editor(|editor, window, cx| {
26016 editor.newline(&Newline, window, cx);
26017 });
26018 cx.wait_for_autoindent_applied().await;
26019 cx.assert_editor_state(indoc! {"
26020 # COMMENT:
26021 ˇ
26022 "});
26023
26024 // test correct indent after newline in brackets
26025 cx.set_state(indoc! {"
26026 {ˇ}
26027 "});
26028 cx.update_editor(|editor, window, cx| {
26029 editor.newline(&Newline, window, cx);
26030 });
26031 cx.wait_for_autoindent_applied().await;
26032 cx.assert_editor_state(indoc! {"
26033 {
26034 ˇ
26035 }
26036 "});
26037
26038 cx.set_state(indoc! {"
26039 (ˇ)
26040 "});
26041 cx.update_editor(|editor, window, cx| {
26042 editor.newline(&Newline, window, cx);
26043 });
26044 cx.run_until_parked();
26045 cx.assert_editor_state(indoc! {"
26046 (
26047 ˇ
26048 )
26049 "});
26050
26051 // do not indent after empty lists or dictionaries
26052 cx.set_state(indoc! {"
26053 a = []ˇ
26054 "});
26055 cx.update_editor(|editor, window, cx| {
26056 editor.newline(&Newline, window, cx);
26057 });
26058 cx.run_until_parked();
26059 cx.assert_editor_state(indoc! {"
26060 a = []
26061 ˇ
26062 "});
26063}
26064
26065#[gpui::test]
26066async fn test_python_indent_in_markdown(cx: &mut TestAppContext) {
26067 init_test(cx, |_| {});
26068
26069 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
26070 let python_lang = languages::language("python", tree_sitter_python::LANGUAGE.into());
26071 language_registry.add(markdown_lang());
26072 language_registry.add(python_lang);
26073
26074 let mut cx = EditorTestContext::new(cx).await;
26075 cx.update_buffer(|buffer, cx| {
26076 buffer.set_language_registry(language_registry);
26077 buffer.set_language(Some(markdown_lang()), cx);
26078 });
26079
26080 // Test that `else:` correctly outdents to match `if:` inside the Python code block
26081 cx.set_state(indoc! {"
26082 # Heading
26083
26084 ```python
26085 def main():
26086 if condition:
26087 pass
26088 ˇ
26089 ```
26090 "});
26091 cx.update_editor(|editor, window, cx| {
26092 editor.handle_input("else:", window, cx);
26093 });
26094 cx.run_until_parked();
26095 cx.assert_editor_state(indoc! {"
26096 # Heading
26097
26098 ```python
26099 def main():
26100 if condition:
26101 pass
26102 else:ˇ
26103 ```
26104 "});
26105}
26106
26107#[gpui::test]
26108async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
26109 init_test(cx, |_| {});
26110
26111 let mut cx = EditorTestContext::new(cx).await;
26112 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26113 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26114
26115 // test cursor move to start of each line on tab
26116 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
26117 cx.set_state(indoc! {"
26118 function main() {
26119 ˇ for item in $items; do
26120 ˇ while [ -n \"$item\" ]; do
26121 ˇ if [ \"$value\" -gt 10 ]; then
26122 ˇ continue
26123 ˇ elif [ \"$value\" -lt 0 ]; then
26124 ˇ break
26125 ˇ else
26126 ˇ echo \"$item\"
26127 ˇ fi
26128 ˇ done
26129 ˇ done
26130 ˇ}
26131 "});
26132 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26133 cx.wait_for_autoindent_applied().await;
26134 cx.assert_editor_state(indoc! {"
26135 function main() {
26136 ˇfor item in $items; do
26137 ˇwhile [ -n \"$item\" ]; do
26138 ˇif [ \"$value\" -gt 10 ]; then
26139 ˇcontinue
26140 ˇelif [ \"$value\" -lt 0 ]; then
26141 ˇbreak
26142 ˇelse
26143 ˇecho \"$item\"
26144 ˇfi
26145 ˇdone
26146 ˇdone
26147 ˇ}
26148 "});
26149 // test relative indent is preserved when tab
26150 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26151 cx.wait_for_autoindent_applied().await;
26152 cx.assert_editor_state(indoc! {"
26153 function main() {
26154 ˇfor item in $items; do
26155 ˇwhile [ -n \"$item\" ]; do
26156 ˇif [ \"$value\" -gt 10 ]; then
26157 ˇcontinue
26158 ˇelif [ \"$value\" -lt 0 ]; then
26159 ˇbreak
26160 ˇelse
26161 ˇecho \"$item\"
26162 ˇfi
26163 ˇdone
26164 ˇdone
26165 ˇ}
26166 "});
26167
26168 // test cursor move to start of each line on tab
26169 // for `case` statement with patterns
26170 cx.set_state(indoc! {"
26171 function handle() {
26172 ˇ case \"$1\" in
26173 ˇ start)
26174 ˇ echo \"a\"
26175 ˇ ;;
26176 ˇ stop)
26177 ˇ echo \"b\"
26178 ˇ ;;
26179 ˇ *)
26180 ˇ echo \"c\"
26181 ˇ ;;
26182 ˇ esac
26183 ˇ}
26184 "});
26185 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26186 cx.wait_for_autoindent_applied().await;
26187 cx.assert_editor_state(indoc! {"
26188 function handle() {
26189 ˇcase \"$1\" in
26190 ˇstart)
26191 ˇecho \"a\"
26192 ˇ;;
26193 ˇstop)
26194 ˇecho \"b\"
26195 ˇ;;
26196 ˇ*)
26197 ˇecho \"c\"
26198 ˇ;;
26199 ˇesac
26200 ˇ}
26201 "});
26202}
26203
26204#[gpui::test]
26205async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
26206 init_test(cx, |_| {});
26207
26208 let mut cx = EditorTestContext::new(cx).await;
26209 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26210 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26211
26212 // test indents on comment insert
26213 cx.set_state(indoc! {"
26214 function main() {
26215 ˇ for item in $items; do
26216 ˇ while [ -n \"$item\" ]; do
26217 ˇ if [ \"$value\" -gt 10 ]; then
26218 ˇ continue
26219 ˇ elif [ \"$value\" -lt 0 ]; then
26220 ˇ break
26221 ˇ else
26222 ˇ echo \"$item\"
26223 ˇ fi
26224 ˇ done
26225 ˇ done
26226 ˇ}
26227 "});
26228 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
26229 cx.wait_for_autoindent_applied().await;
26230 cx.assert_editor_state(indoc! {"
26231 function main() {
26232 #ˇ for item in $items; do
26233 #ˇ while [ -n \"$item\" ]; do
26234 #ˇ if [ \"$value\" -gt 10 ]; then
26235 #ˇ continue
26236 #ˇ elif [ \"$value\" -lt 0 ]; then
26237 #ˇ break
26238 #ˇ else
26239 #ˇ echo \"$item\"
26240 #ˇ fi
26241 #ˇ done
26242 #ˇ done
26243 #ˇ}
26244 "});
26245}
26246
26247#[gpui::test]
26248async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
26249 init_test(cx, |_| {});
26250
26251 let mut cx = EditorTestContext::new(cx).await;
26252 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26253 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26254
26255 // test `else` auto outdents when typed inside `if` block
26256 cx.set_state(indoc! {"
26257 if [ \"$1\" = \"test\" ]; then
26258 echo \"foo bar\"
26259 ˇ
26260 "});
26261 cx.update_editor(|editor, window, cx| {
26262 editor.handle_input("else", window, cx);
26263 });
26264 cx.wait_for_autoindent_applied().await;
26265 cx.assert_editor_state(indoc! {"
26266 if [ \"$1\" = \"test\" ]; then
26267 echo \"foo bar\"
26268 elseˇ
26269 "});
26270
26271 // test `elif` auto outdents when typed inside `if` block
26272 cx.set_state(indoc! {"
26273 if [ \"$1\" = \"test\" ]; then
26274 echo \"foo bar\"
26275 ˇ
26276 "});
26277 cx.update_editor(|editor, window, cx| {
26278 editor.handle_input("elif", window, cx);
26279 });
26280 cx.wait_for_autoindent_applied().await;
26281 cx.assert_editor_state(indoc! {"
26282 if [ \"$1\" = \"test\" ]; then
26283 echo \"foo bar\"
26284 elifˇ
26285 "});
26286
26287 // test `fi` auto outdents when typed inside `else` block
26288 cx.set_state(indoc! {"
26289 if [ \"$1\" = \"test\" ]; then
26290 echo \"foo bar\"
26291 else
26292 echo \"bar baz\"
26293 ˇ
26294 "});
26295 cx.update_editor(|editor, window, cx| {
26296 editor.handle_input("fi", window, cx);
26297 });
26298 cx.wait_for_autoindent_applied().await;
26299 cx.assert_editor_state(indoc! {"
26300 if [ \"$1\" = \"test\" ]; then
26301 echo \"foo bar\"
26302 else
26303 echo \"bar baz\"
26304 fiˇ
26305 "});
26306
26307 // test `done` auto outdents when typed inside `while` block
26308 cx.set_state(indoc! {"
26309 while read line; do
26310 echo \"$line\"
26311 ˇ
26312 "});
26313 cx.update_editor(|editor, window, cx| {
26314 editor.handle_input("done", window, cx);
26315 });
26316 cx.wait_for_autoindent_applied().await;
26317 cx.assert_editor_state(indoc! {"
26318 while read line; do
26319 echo \"$line\"
26320 doneˇ
26321 "});
26322
26323 // test `done` auto outdents when typed inside `for` block
26324 cx.set_state(indoc! {"
26325 for file in *.txt; do
26326 cat \"$file\"
26327 ˇ
26328 "});
26329 cx.update_editor(|editor, window, cx| {
26330 editor.handle_input("done", window, cx);
26331 });
26332 cx.wait_for_autoindent_applied().await;
26333 cx.assert_editor_state(indoc! {"
26334 for file in *.txt; do
26335 cat \"$file\"
26336 doneˇ
26337 "});
26338
26339 // test `esac` auto outdents when typed inside `case` block
26340 cx.set_state(indoc! {"
26341 case \"$1\" in
26342 start)
26343 echo \"foo bar\"
26344 ;;
26345 stop)
26346 echo \"bar baz\"
26347 ;;
26348 ˇ
26349 "});
26350 cx.update_editor(|editor, window, cx| {
26351 editor.handle_input("esac", window, cx);
26352 });
26353 cx.wait_for_autoindent_applied().await;
26354 cx.assert_editor_state(indoc! {"
26355 case \"$1\" in
26356 start)
26357 echo \"foo bar\"
26358 ;;
26359 stop)
26360 echo \"bar baz\"
26361 ;;
26362 esacˇ
26363 "});
26364
26365 // test `*)` auto outdents when typed inside `case` block
26366 cx.set_state(indoc! {"
26367 case \"$1\" in
26368 start)
26369 echo \"foo bar\"
26370 ;;
26371 ˇ
26372 "});
26373 cx.update_editor(|editor, window, cx| {
26374 editor.handle_input("*)", window, cx);
26375 });
26376 cx.wait_for_autoindent_applied().await;
26377 cx.assert_editor_state(indoc! {"
26378 case \"$1\" in
26379 start)
26380 echo \"foo bar\"
26381 ;;
26382 *)ˇ
26383 "});
26384
26385 // test `fi` outdents to correct level with nested if blocks
26386 cx.set_state(indoc! {"
26387 if [ \"$1\" = \"test\" ]; then
26388 echo \"outer if\"
26389 if [ \"$2\" = \"debug\" ]; then
26390 echo \"inner if\"
26391 ˇ
26392 "});
26393 cx.update_editor(|editor, window, cx| {
26394 editor.handle_input("fi", window, cx);
26395 });
26396 cx.wait_for_autoindent_applied().await;
26397 cx.assert_editor_state(indoc! {"
26398 if [ \"$1\" = \"test\" ]; then
26399 echo \"outer if\"
26400 if [ \"$2\" = \"debug\" ]; then
26401 echo \"inner if\"
26402 fiˇ
26403 "});
26404}
26405
26406#[gpui::test]
26407async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
26408 init_test(cx, |_| {});
26409 update_test_language_settings(cx, |settings| {
26410 settings.defaults.extend_comment_on_newline = Some(false);
26411 });
26412 let mut cx = EditorTestContext::new(cx).await;
26413 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26414 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26415
26416 // test correct indent after newline on comment
26417 cx.set_state(indoc! {"
26418 # COMMENT:ˇ
26419 "});
26420 cx.update_editor(|editor, window, cx| {
26421 editor.newline(&Newline, window, cx);
26422 });
26423 cx.wait_for_autoindent_applied().await;
26424 cx.assert_editor_state(indoc! {"
26425 # COMMENT:
26426 ˇ
26427 "});
26428
26429 // test correct indent after newline after `then`
26430 cx.set_state(indoc! {"
26431
26432 if [ \"$1\" = \"test\" ]; thenˇ
26433 "});
26434 cx.update_editor(|editor, window, cx| {
26435 editor.newline(&Newline, window, cx);
26436 });
26437 cx.wait_for_autoindent_applied().await;
26438 cx.assert_editor_state(indoc! {"
26439
26440 if [ \"$1\" = \"test\" ]; then
26441 ˇ
26442 "});
26443
26444 // test correct indent after newline after `else`
26445 cx.set_state(indoc! {"
26446 if [ \"$1\" = \"test\" ]; then
26447 elseˇ
26448 "});
26449 cx.update_editor(|editor, window, cx| {
26450 editor.newline(&Newline, window, cx);
26451 });
26452 cx.wait_for_autoindent_applied().await;
26453 cx.assert_editor_state(indoc! {"
26454 if [ \"$1\" = \"test\" ]; then
26455 else
26456 ˇ
26457 "});
26458
26459 // test correct indent after newline after `elif`
26460 cx.set_state(indoc! {"
26461 if [ \"$1\" = \"test\" ]; then
26462 elifˇ
26463 "});
26464 cx.update_editor(|editor, window, cx| {
26465 editor.newline(&Newline, window, cx);
26466 });
26467 cx.wait_for_autoindent_applied().await;
26468 cx.assert_editor_state(indoc! {"
26469 if [ \"$1\" = \"test\" ]; then
26470 elif
26471 ˇ
26472 "});
26473
26474 // test correct indent after newline after `do`
26475 cx.set_state(indoc! {"
26476 for file in *.txt; doˇ
26477 "});
26478 cx.update_editor(|editor, window, cx| {
26479 editor.newline(&Newline, window, cx);
26480 });
26481 cx.wait_for_autoindent_applied().await;
26482 cx.assert_editor_state(indoc! {"
26483 for file in *.txt; do
26484 ˇ
26485 "});
26486
26487 // test correct indent after newline after case pattern
26488 cx.set_state(indoc! {"
26489 case \"$1\" in
26490 start)ˇ
26491 "});
26492 cx.update_editor(|editor, window, cx| {
26493 editor.newline(&Newline, window, cx);
26494 });
26495 cx.wait_for_autoindent_applied().await;
26496 cx.assert_editor_state(indoc! {"
26497 case \"$1\" in
26498 start)
26499 ˇ
26500 "});
26501
26502 // test correct indent after newline after case pattern
26503 cx.set_state(indoc! {"
26504 case \"$1\" in
26505 start)
26506 ;;
26507 *)ˇ
26508 "});
26509 cx.update_editor(|editor, window, cx| {
26510 editor.newline(&Newline, window, cx);
26511 });
26512 cx.wait_for_autoindent_applied().await;
26513 cx.assert_editor_state(indoc! {"
26514 case \"$1\" in
26515 start)
26516 ;;
26517 *)
26518 ˇ
26519 "});
26520
26521 // test correct indent after newline after function opening brace
26522 cx.set_state(indoc! {"
26523 function test() {ˇ}
26524 "});
26525 cx.update_editor(|editor, window, cx| {
26526 editor.newline(&Newline, window, cx);
26527 });
26528 cx.wait_for_autoindent_applied().await;
26529 cx.assert_editor_state(indoc! {"
26530 function test() {
26531 ˇ
26532 }
26533 "});
26534
26535 // test no extra indent after semicolon on same line
26536 cx.set_state(indoc! {"
26537 echo \"test\";ˇ
26538 "});
26539 cx.update_editor(|editor, window, cx| {
26540 editor.newline(&Newline, window, cx);
26541 });
26542 cx.wait_for_autoindent_applied().await;
26543 cx.assert_editor_state(indoc! {"
26544 echo \"test\";
26545 ˇ
26546 "});
26547}
26548
26549fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
26550 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
26551 point..point
26552}
26553
26554#[track_caller]
26555fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
26556 let (text, ranges) = marked_text_ranges(marked_text, true);
26557 assert_eq!(editor.text(cx), text);
26558 assert_eq!(
26559 editor.selections.ranges(&editor.display_snapshot(cx)),
26560 ranges
26561 .iter()
26562 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
26563 .collect::<Vec<_>>(),
26564 "Assert selections are {}",
26565 marked_text
26566 );
26567}
26568
26569pub fn handle_signature_help_request(
26570 cx: &mut EditorLspTestContext,
26571 mocked_response: lsp::SignatureHelp,
26572) -> impl Future<Output = ()> + use<> {
26573 let mut request =
26574 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
26575 let mocked_response = mocked_response.clone();
26576 async move { Ok(Some(mocked_response)) }
26577 });
26578
26579 async move {
26580 request.next().await;
26581 }
26582}
26583
26584#[track_caller]
26585pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
26586 cx.update_editor(|editor, _, _| {
26587 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
26588 let entries = menu.entries.borrow();
26589 let entries = entries
26590 .iter()
26591 .map(|entry| entry.string.as_str())
26592 .collect::<Vec<_>>();
26593 assert_eq!(entries, expected);
26594 } else {
26595 panic!("Expected completions menu");
26596 }
26597 });
26598}
26599
26600#[gpui::test]
26601async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
26602 init_test(cx, |_| {});
26603 let mut cx = EditorLspTestContext::new_rust(
26604 lsp::ServerCapabilities {
26605 completion_provider: Some(lsp::CompletionOptions {
26606 ..Default::default()
26607 }),
26608 ..Default::default()
26609 },
26610 cx,
26611 )
26612 .await;
26613 cx.lsp
26614 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
26615 Ok(Some(lsp::CompletionResponse::Array(vec![
26616 lsp::CompletionItem {
26617 label: "unsafe".into(),
26618 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26619 range: lsp::Range {
26620 start: lsp::Position {
26621 line: 0,
26622 character: 9,
26623 },
26624 end: lsp::Position {
26625 line: 0,
26626 character: 11,
26627 },
26628 },
26629 new_text: "unsafe".to_string(),
26630 })),
26631 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
26632 ..Default::default()
26633 },
26634 ])))
26635 });
26636
26637 cx.update_editor(|editor, _, cx| {
26638 editor.project().unwrap().update(cx, |project, cx| {
26639 project.snippets().update(cx, |snippets, _cx| {
26640 snippets.add_snippet_for_test(
26641 None,
26642 PathBuf::from("test_snippets.json"),
26643 vec![
26644 Arc::new(project::snippet_provider::Snippet {
26645 prefix: vec![
26646 "unlimited word count".to_string(),
26647 "unlimit word count".to_string(),
26648 "unlimited unknown".to_string(),
26649 ],
26650 body: "this is many words".to_string(),
26651 description: Some("description".to_string()),
26652 name: "multi-word snippet test".to_string(),
26653 }),
26654 Arc::new(project::snippet_provider::Snippet {
26655 prefix: vec!["unsnip".to_string(), "@few".to_string()],
26656 body: "fewer words".to_string(),
26657 description: Some("alt description".to_string()),
26658 name: "other name".to_string(),
26659 }),
26660 Arc::new(project::snippet_provider::Snippet {
26661 prefix: vec!["ab aa".to_string()],
26662 body: "abcd".to_string(),
26663 description: None,
26664 name: "alphabet".to_string(),
26665 }),
26666 ],
26667 );
26668 });
26669 })
26670 });
26671
26672 let get_completions = |cx: &mut EditorLspTestContext| {
26673 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
26674 Some(CodeContextMenu::Completions(context_menu)) => {
26675 let entries = context_menu.entries.borrow();
26676 entries
26677 .iter()
26678 .map(|entry| entry.string.clone())
26679 .collect_vec()
26680 }
26681 _ => vec![],
26682 })
26683 };
26684
26685 // snippets:
26686 // @foo
26687 // foo bar
26688 //
26689 // when typing:
26690 //
26691 // when typing:
26692 // - if I type a symbol "open the completions with snippets only"
26693 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
26694 //
26695 // stuff we need:
26696 // - filtering logic change?
26697 // - remember how far back the completion started.
26698
26699 let test_cases: &[(&str, &[&str])] = &[
26700 (
26701 "un",
26702 &[
26703 "unsafe",
26704 "unlimit word count",
26705 "unlimited unknown",
26706 "unlimited word count",
26707 "unsnip",
26708 ],
26709 ),
26710 (
26711 "u ",
26712 &[
26713 "unlimit word count",
26714 "unlimited unknown",
26715 "unlimited word count",
26716 ],
26717 ),
26718 ("u a", &["ab aa", "unsafe"]), // unsAfe
26719 (
26720 "u u",
26721 &[
26722 "unsafe",
26723 "unlimit word count",
26724 "unlimited unknown", // ranked highest among snippets
26725 "unlimited word count",
26726 "unsnip",
26727 ],
26728 ),
26729 ("uw c", &["unlimit word count", "unlimited word count"]),
26730 (
26731 "u w",
26732 &[
26733 "unlimit word count",
26734 "unlimited word count",
26735 "unlimited unknown",
26736 ],
26737 ),
26738 ("u w ", &["unlimit word count", "unlimited word count"]),
26739 (
26740 "u ",
26741 &[
26742 "unlimit word count",
26743 "unlimited unknown",
26744 "unlimited word count",
26745 ],
26746 ),
26747 ("wor", &[]),
26748 ("uf", &["unsafe"]),
26749 ("af", &["unsafe"]),
26750 ("afu", &[]),
26751 (
26752 "ue",
26753 &["unsafe", "unlimited unknown", "unlimited word count"],
26754 ),
26755 ("@", &["@few"]),
26756 ("@few", &["@few"]),
26757 ("@ ", &[]),
26758 ("a@", &["@few"]),
26759 ("a@f", &["@few", "unsafe"]),
26760 ("a@fw", &["@few"]),
26761 ("a", &["ab aa", "unsafe"]),
26762 ("aa", &["ab aa"]),
26763 ("aaa", &["ab aa"]),
26764 ("ab", &["ab aa"]),
26765 ("ab ", &["ab aa"]),
26766 ("ab a", &["ab aa", "unsafe"]),
26767 ("ab ab", &["ab aa"]),
26768 ("ab ab aa", &["ab aa"]),
26769 ];
26770
26771 for &(input_to_simulate, expected_completions) in test_cases {
26772 cx.set_state("fn a() { ˇ }\n");
26773 for c in input_to_simulate.split("") {
26774 cx.simulate_input(c);
26775 cx.run_until_parked();
26776 }
26777 let expected_completions = expected_completions
26778 .iter()
26779 .map(|s| s.to_string())
26780 .collect_vec();
26781 assert_eq!(
26782 get_completions(&mut cx),
26783 expected_completions,
26784 "< actual / expected >, input = {input_to_simulate:?}",
26785 );
26786 }
26787}
26788
26789/// Handle completion request passing a marked string specifying where the completion
26790/// should be triggered from using '|' character, what range should be replaced, and what completions
26791/// should be returned using '<' and '>' to delimit the range.
26792///
26793/// Also see `handle_completion_request_with_insert_and_replace`.
26794#[track_caller]
26795pub fn handle_completion_request(
26796 marked_string: &str,
26797 completions: Vec<&'static str>,
26798 is_incomplete: bool,
26799 counter: Arc<AtomicUsize>,
26800 cx: &mut EditorLspTestContext,
26801) -> impl Future<Output = ()> {
26802 let complete_from_marker: TextRangeMarker = '|'.into();
26803 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26804 let (_, mut marked_ranges) = marked_text_ranges_by(
26805 marked_string,
26806 vec![complete_from_marker.clone(), replace_range_marker.clone()],
26807 );
26808
26809 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26810 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26811 ));
26812 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26813 let replace_range =
26814 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26815
26816 let mut request =
26817 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26818 let completions = completions.clone();
26819 counter.fetch_add(1, atomic::Ordering::Release);
26820 async move {
26821 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26822 assert_eq!(
26823 params.text_document_position.position,
26824 complete_from_position
26825 );
26826 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
26827 is_incomplete,
26828 item_defaults: None,
26829 items: completions
26830 .iter()
26831 .map(|completion_text| lsp::CompletionItem {
26832 label: completion_text.to_string(),
26833 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
26834 range: replace_range,
26835 new_text: completion_text.to_string(),
26836 })),
26837 ..Default::default()
26838 })
26839 .collect(),
26840 })))
26841 }
26842 });
26843
26844 async move {
26845 request.next().await;
26846 }
26847}
26848
26849/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
26850/// given instead, which also contains an `insert` range.
26851///
26852/// This function uses markers to define ranges:
26853/// - `|` marks the cursor position
26854/// - `<>` marks the replace range
26855/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
26856pub fn handle_completion_request_with_insert_and_replace(
26857 cx: &mut EditorLspTestContext,
26858 marked_string: &str,
26859 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
26860 counter: Arc<AtomicUsize>,
26861) -> impl Future<Output = ()> {
26862 let complete_from_marker: TextRangeMarker = '|'.into();
26863 let replace_range_marker: TextRangeMarker = ('<', '>').into();
26864 let insert_range_marker: TextRangeMarker = ('{', '}').into();
26865
26866 let (_, mut marked_ranges) = marked_text_ranges_by(
26867 marked_string,
26868 vec![
26869 complete_from_marker.clone(),
26870 replace_range_marker.clone(),
26871 insert_range_marker.clone(),
26872 ],
26873 );
26874
26875 let complete_from_position = cx.to_lsp(MultiBufferOffset(
26876 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
26877 ));
26878 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
26879 let replace_range =
26880 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
26881
26882 let insert_range = match marked_ranges.remove(&insert_range_marker) {
26883 Some(ranges) if !ranges.is_empty() => {
26884 let range1 = ranges[0].clone();
26885 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
26886 }
26887 _ => lsp::Range {
26888 start: replace_range.start,
26889 end: complete_from_position,
26890 },
26891 };
26892
26893 let mut request =
26894 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
26895 let completions = completions.clone();
26896 counter.fetch_add(1, atomic::Ordering::Release);
26897 async move {
26898 assert_eq!(params.text_document_position.text_document.uri, url.clone());
26899 assert_eq!(
26900 params.text_document_position.position, complete_from_position,
26901 "marker `|` position doesn't match",
26902 );
26903 Ok(Some(lsp::CompletionResponse::Array(
26904 completions
26905 .iter()
26906 .map(|(label, new_text)| lsp::CompletionItem {
26907 label: label.to_string(),
26908 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
26909 lsp::InsertReplaceEdit {
26910 insert: insert_range,
26911 replace: replace_range,
26912 new_text: new_text.to_string(),
26913 },
26914 )),
26915 ..Default::default()
26916 })
26917 .collect(),
26918 )))
26919 }
26920 });
26921
26922 async move {
26923 request.next().await;
26924 }
26925}
26926
26927fn handle_resolve_completion_request(
26928 cx: &mut EditorLspTestContext,
26929 edits: Option<Vec<(&'static str, &'static str)>>,
26930) -> impl Future<Output = ()> {
26931 let edits = edits.map(|edits| {
26932 edits
26933 .iter()
26934 .map(|(marked_string, new_text)| {
26935 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
26936 let replace_range = cx.to_lsp_range(
26937 MultiBufferOffset(marked_ranges[0].start)
26938 ..MultiBufferOffset(marked_ranges[0].end),
26939 );
26940 lsp::TextEdit::new(replace_range, new_text.to_string())
26941 })
26942 .collect::<Vec<_>>()
26943 });
26944
26945 let mut request =
26946 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
26947 let edits = edits.clone();
26948 async move {
26949 Ok(lsp::CompletionItem {
26950 additional_text_edits: edits,
26951 ..Default::default()
26952 })
26953 }
26954 });
26955
26956 async move {
26957 request.next().await;
26958 }
26959}
26960
26961pub(crate) fn update_test_language_settings(
26962 cx: &mut TestAppContext,
26963 f: impl Fn(&mut AllLanguageSettingsContent),
26964) {
26965 cx.update(|cx| {
26966 SettingsStore::update_global(cx, |store, cx| {
26967 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
26968 });
26969 });
26970}
26971
26972pub(crate) fn update_test_project_settings(
26973 cx: &mut TestAppContext,
26974 f: impl Fn(&mut ProjectSettingsContent),
26975) {
26976 cx.update(|cx| {
26977 SettingsStore::update_global(cx, |store, cx| {
26978 store.update_user_settings(cx, |settings| f(&mut settings.project));
26979 });
26980 });
26981}
26982
26983pub(crate) fn update_test_editor_settings(
26984 cx: &mut TestAppContext,
26985 f: impl Fn(&mut EditorSettingsContent),
26986) {
26987 cx.update(|cx| {
26988 SettingsStore::update_global(cx, |store, cx| {
26989 store.update_user_settings(cx, |settings| f(&mut settings.editor));
26990 })
26991 })
26992}
26993
26994pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
26995 cx.update(|cx| {
26996 assets::Assets.load_test_fonts(cx);
26997 let store = SettingsStore::test(cx);
26998 cx.set_global(store);
26999 theme::init(theme::LoadThemes::JustBase, cx);
27000 release_channel::init(semver::Version::new(0, 0, 0), cx);
27001 crate::init(cx);
27002 });
27003 zlog::init_test();
27004 update_test_language_settings(cx, f);
27005}
27006
27007#[track_caller]
27008fn assert_hunk_revert(
27009 not_reverted_text_with_selections: &str,
27010 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
27011 expected_reverted_text_with_selections: &str,
27012 base_text: &str,
27013 cx: &mut EditorLspTestContext,
27014) {
27015 cx.set_state(not_reverted_text_with_selections);
27016 cx.set_head_text(base_text);
27017 cx.executor().run_until_parked();
27018
27019 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
27020 let snapshot = editor.snapshot(window, cx);
27021 let reverted_hunk_statuses = snapshot
27022 .buffer_snapshot()
27023 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
27024 .map(|hunk| hunk.status().kind)
27025 .collect::<Vec<_>>();
27026
27027 editor.git_restore(&Default::default(), window, cx);
27028 reverted_hunk_statuses
27029 });
27030 cx.executor().run_until_parked();
27031 cx.assert_editor_state(expected_reverted_text_with_selections);
27032 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
27033}
27034
27035#[gpui::test(iterations = 10)]
27036async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
27037 init_test(cx, |_| {});
27038
27039 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
27040 let counter = diagnostic_requests.clone();
27041
27042 let fs = FakeFs::new(cx.executor());
27043 fs.insert_tree(
27044 path!("/a"),
27045 json!({
27046 "first.rs": "fn main() { let a = 5; }",
27047 "second.rs": "// Test file",
27048 }),
27049 )
27050 .await;
27051
27052 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27053 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27054 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27055
27056 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27057 language_registry.add(rust_lang());
27058 let mut fake_servers = language_registry.register_fake_lsp(
27059 "Rust",
27060 FakeLspAdapter {
27061 capabilities: lsp::ServerCapabilities {
27062 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
27063 lsp::DiagnosticOptions {
27064 identifier: None,
27065 inter_file_dependencies: true,
27066 workspace_diagnostics: true,
27067 work_done_progress_options: Default::default(),
27068 },
27069 )),
27070 ..Default::default()
27071 },
27072 ..Default::default()
27073 },
27074 );
27075
27076 let editor = workspace
27077 .update(cx, |workspace, window, cx| {
27078 workspace.open_abs_path(
27079 PathBuf::from(path!("/a/first.rs")),
27080 OpenOptions::default(),
27081 window,
27082 cx,
27083 )
27084 })
27085 .unwrap()
27086 .await
27087 .unwrap()
27088 .downcast::<Editor>()
27089 .unwrap();
27090 let fake_server = fake_servers.next().await.unwrap();
27091 let server_id = fake_server.server.server_id();
27092 let mut first_request = fake_server
27093 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
27094 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
27095 let result_id = Some(new_result_id.to_string());
27096 assert_eq!(
27097 params.text_document.uri,
27098 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27099 );
27100 async move {
27101 Ok(lsp::DocumentDiagnosticReportResult::Report(
27102 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
27103 related_documents: None,
27104 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
27105 items: Vec::new(),
27106 result_id,
27107 },
27108 }),
27109 ))
27110 }
27111 });
27112
27113 let ensure_result_id = |expected: Option<SharedString>, cx: &mut TestAppContext| {
27114 project.update(cx, |project, cx| {
27115 let buffer_id = editor
27116 .read(cx)
27117 .buffer()
27118 .read(cx)
27119 .as_singleton()
27120 .expect("created a singleton buffer")
27121 .read(cx)
27122 .remote_id();
27123 let buffer_result_id = project
27124 .lsp_store()
27125 .read(cx)
27126 .result_id_for_buffer_pull(server_id, buffer_id, &None, cx);
27127 assert_eq!(expected, buffer_result_id);
27128 });
27129 };
27130
27131 ensure_result_id(None, cx);
27132 cx.executor().advance_clock(Duration::from_millis(60));
27133 cx.executor().run_until_parked();
27134 assert_eq!(
27135 diagnostic_requests.load(atomic::Ordering::Acquire),
27136 1,
27137 "Opening file should trigger diagnostic request"
27138 );
27139 first_request
27140 .next()
27141 .await
27142 .expect("should have sent the first diagnostics pull request");
27143 ensure_result_id(Some(SharedString::new("1")), cx);
27144
27145 // Editing should trigger diagnostics
27146 editor.update_in(cx, |editor, window, cx| {
27147 editor.handle_input("2", window, cx)
27148 });
27149 cx.executor().advance_clock(Duration::from_millis(60));
27150 cx.executor().run_until_parked();
27151 assert_eq!(
27152 diagnostic_requests.load(atomic::Ordering::Acquire),
27153 2,
27154 "Editing should trigger diagnostic request"
27155 );
27156 ensure_result_id(Some(SharedString::new("2")), cx);
27157
27158 // Moving cursor should not trigger diagnostic request
27159 editor.update_in(cx, |editor, window, cx| {
27160 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27161 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
27162 });
27163 });
27164 cx.executor().advance_clock(Duration::from_millis(60));
27165 cx.executor().run_until_parked();
27166 assert_eq!(
27167 diagnostic_requests.load(atomic::Ordering::Acquire),
27168 2,
27169 "Cursor movement should not trigger diagnostic request"
27170 );
27171 ensure_result_id(Some(SharedString::new("2")), cx);
27172 // Multiple rapid edits should be debounced
27173 for _ in 0..5 {
27174 editor.update_in(cx, |editor, window, cx| {
27175 editor.handle_input("x", window, cx)
27176 });
27177 }
27178 cx.executor().advance_clock(Duration::from_millis(60));
27179 cx.executor().run_until_parked();
27180
27181 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
27182 assert!(
27183 final_requests <= 4,
27184 "Multiple rapid edits should be debounced (got {final_requests} requests)",
27185 );
27186 ensure_result_id(Some(SharedString::new(final_requests.to_string())), cx);
27187}
27188
27189#[gpui::test]
27190async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
27191 // Regression test for issue #11671
27192 // Previously, adding a cursor after moving multiple cursors would reset
27193 // the cursor count instead of adding to the existing cursors.
27194 init_test(cx, |_| {});
27195 let mut cx = EditorTestContext::new(cx).await;
27196
27197 // Create a simple buffer with cursor at start
27198 cx.set_state(indoc! {"
27199 ˇaaaa
27200 bbbb
27201 cccc
27202 dddd
27203 eeee
27204 ffff
27205 gggg
27206 hhhh"});
27207
27208 // Add 2 cursors below (so we have 3 total)
27209 cx.update_editor(|editor, window, cx| {
27210 editor.add_selection_below(&Default::default(), window, cx);
27211 editor.add_selection_below(&Default::default(), window, cx);
27212 });
27213
27214 // Verify we have 3 cursors
27215 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
27216 assert_eq!(
27217 initial_count, 3,
27218 "Should have 3 cursors after adding 2 below"
27219 );
27220
27221 // Move down one line
27222 cx.update_editor(|editor, window, cx| {
27223 editor.move_down(&MoveDown, window, cx);
27224 });
27225
27226 // Add another cursor below
27227 cx.update_editor(|editor, window, cx| {
27228 editor.add_selection_below(&Default::default(), window, cx);
27229 });
27230
27231 // Should now have 4 cursors (3 original + 1 new)
27232 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
27233 assert_eq!(
27234 final_count, 4,
27235 "Should have 4 cursors after moving and adding another"
27236 );
27237}
27238
27239#[gpui::test]
27240async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
27241 init_test(cx, |_| {});
27242
27243 let mut cx = EditorTestContext::new(cx).await;
27244
27245 cx.set_state(indoc!(
27246 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
27247 Second line here"#
27248 ));
27249
27250 cx.update_editor(|editor, window, cx| {
27251 // Enable soft wrapping with a narrow width to force soft wrapping and
27252 // confirm that more than 2 rows are being displayed.
27253 editor.set_wrap_width(Some(100.0.into()), cx);
27254 assert!(editor.display_text(cx).lines().count() > 2);
27255
27256 editor.add_selection_below(
27257 &AddSelectionBelow {
27258 skip_soft_wrap: true,
27259 },
27260 window,
27261 cx,
27262 );
27263
27264 assert_eq!(
27265 display_ranges(editor, cx),
27266 &[
27267 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
27268 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
27269 ]
27270 );
27271
27272 editor.add_selection_above(
27273 &AddSelectionAbove {
27274 skip_soft_wrap: true,
27275 },
27276 window,
27277 cx,
27278 );
27279
27280 assert_eq!(
27281 display_ranges(editor, cx),
27282 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27283 );
27284
27285 editor.add_selection_below(
27286 &AddSelectionBelow {
27287 skip_soft_wrap: false,
27288 },
27289 window,
27290 cx,
27291 );
27292
27293 assert_eq!(
27294 display_ranges(editor, cx),
27295 &[
27296 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
27297 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
27298 ]
27299 );
27300
27301 editor.add_selection_above(
27302 &AddSelectionAbove {
27303 skip_soft_wrap: false,
27304 },
27305 window,
27306 cx,
27307 );
27308
27309 assert_eq!(
27310 display_ranges(editor, cx),
27311 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27312 );
27313 });
27314}
27315
27316#[gpui::test]
27317async fn test_insert_snippet(cx: &mut TestAppContext) {
27318 init_test(cx, |_| {});
27319 let mut cx = EditorTestContext::new(cx).await;
27320
27321 cx.update_editor(|editor, _, cx| {
27322 editor.project().unwrap().update(cx, |project, cx| {
27323 project.snippets().update(cx, |snippets, _cx| {
27324 let snippet = project::snippet_provider::Snippet {
27325 prefix: vec![], // no prefix needed!
27326 body: "an Unspecified".to_string(),
27327 description: Some("shhhh it's a secret".to_string()),
27328 name: "super secret snippet".to_string(),
27329 };
27330 snippets.add_snippet_for_test(
27331 None,
27332 PathBuf::from("test_snippets.json"),
27333 vec![Arc::new(snippet)],
27334 );
27335
27336 let snippet = project::snippet_provider::Snippet {
27337 prefix: vec![], // no prefix needed!
27338 body: " Location".to_string(),
27339 description: Some("the word 'location'".to_string()),
27340 name: "location word".to_string(),
27341 };
27342 snippets.add_snippet_for_test(
27343 Some("Markdown".to_string()),
27344 PathBuf::from("test_snippets.json"),
27345 vec![Arc::new(snippet)],
27346 );
27347 });
27348 })
27349 });
27350
27351 cx.set_state(indoc!(r#"First cursor at ˇ and second cursor at ˇ"#));
27352
27353 cx.update_editor(|editor, window, cx| {
27354 editor.insert_snippet_at_selections(
27355 &InsertSnippet {
27356 language: None,
27357 name: Some("super secret snippet".to_string()),
27358 snippet: None,
27359 },
27360 window,
27361 cx,
27362 );
27363
27364 // Language is specified in the action,
27365 // so the buffer language does not need to match
27366 editor.insert_snippet_at_selections(
27367 &InsertSnippet {
27368 language: Some("Markdown".to_string()),
27369 name: Some("location word".to_string()),
27370 snippet: None,
27371 },
27372 window,
27373 cx,
27374 );
27375
27376 editor.insert_snippet_at_selections(
27377 &InsertSnippet {
27378 language: None,
27379 name: None,
27380 snippet: Some("$0 after".to_string()),
27381 },
27382 window,
27383 cx,
27384 );
27385 });
27386
27387 cx.assert_editor_state(
27388 r#"First cursor at an Unspecified Locationˇ after and second cursor at an Unspecified Locationˇ after"#,
27389 );
27390}
27391
27392#[gpui::test(iterations = 10)]
27393async fn test_document_colors(cx: &mut TestAppContext) {
27394 let expected_color = Rgba {
27395 r: 0.33,
27396 g: 0.33,
27397 b: 0.33,
27398 a: 0.33,
27399 };
27400
27401 init_test(cx, |_| {});
27402
27403 let fs = FakeFs::new(cx.executor());
27404 fs.insert_tree(
27405 path!("/a"),
27406 json!({
27407 "first.rs": "fn main() { let a = 5; }",
27408 }),
27409 )
27410 .await;
27411
27412 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27413 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27414 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27415
27416 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27417 language_registry.add(rust_lang());
27418 let mut fake_servers = language_registry.register_fake_lsp(
27419 "Rust",
27420 FakeLspAdapter {
27421 capabilities: lsp::ServerCapabilities {
27422 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
27423 ..lsp::ServerCapabilities::default()
27424 },
27425 name: "rust-analyzer",
27426 ..FakeLspAdapter::default()
27427 },
27428 );
27429 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
27430 "Rust",
27431 FakeLspAdapter {
27432 capabilities: lsp::ServerCapabilities {
27433 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
27434 ..lsp::ServerCapabilities::default()
27435 },
27436 name: "not-rust-analyzer",
27437 ..FakeLspAdapter::default()
27438 },
27439 );
27440
27441 let editor = workspace
27442 .update(cx, |workspace, window, cx| {
27443 workspace.open_abs_path(
27444 PathBuf::from(path!("/a/first.rs")),
27445 OpenOptions::default(),
27446 window,
27447 cx,
27448 )
27449 })
27450 .unwrap()
27451 .await
27452 .unwrap()
27453 .downcast::<Editor>()
27454 .unwrap();
27455 let fake_language_server = fake_servers.next().await.unwrap();
27456 let fake_language_server_without_capabilities =
27457 fake_servers_without_capabilities.next().await.unwrap();
27458 let requests_made = Arc::new(AtomicUsize::new(0));
27459 let closure_requests_made = Arc::clone(&requests_made);
27460 let mut color_request_handle = fake_language_server
27461 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27462 let requests_made = Arc::clone(&closure_requests_made);
27463 async move {
27464 assert_eq!(
27465 params.text_document.uri,
27466 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27467 );
27468 requests_made.fetch_add(1, atomic::Ordering::Release);
27469 Ok(vec![
27470 lsp::ColorInformation {
27471 range: lsp::Range {
27472 start: lsp::Position {
27473 line: 0,
27474 character: 0,
27475 },
27476 end: lsp::Position {
27477 line: 0,
27478 character: 1,
27479 },
27480 },
27481 color: lsp::Color {
27482 red: 0.33,
27483 green: 0.33,
27484 blue: 0.33,
27485 alpha: 0.33,
27486 },
27487 },
27488 lsp::ColorInformation {
27489 range: lsp::Range {
27490 start: lsp::Position {
27491 line: 0,
27492 character: 0,
27493 },
27494 end: lsp::Position {
27495 line: 0,
27496 character: 1,
27497 },
27498 },
27499 color: lsp::Color {
27500 red: 0.33,
27501 green: 0.33,
27502 blue: 0.33,
27503 alpha: 0.33,
27504 },
27505 },
27506 ])
27507 }
27508 });
27509
27510 let _handle = fake_language_server_without_capabilities
27511 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
27512 panic!("Should not be called");
27513 });
27514 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27515 color_request_handle.next().await.unwrap();
27516 cx.run_until_parked();
27517 assert_eq!(
27518 1,
27519 requests_made.load(atomic::Ordering::Acquire),
27520 "Should query for colors once per editor open"
27521 );
27522 editor.update_in(cx, |editor, _, cx| {
27523 assert_eq!(
27524 vec![expected_color],
27525 extract_color_inlays(editor, cx),
27526 "Should have an initial inlay"
27527 );
27528 });
27529
27530 // opening another file in a split should not influence the LSP query counter
27531 workspace
27532 .update(cx, |workspace, window, cx| {
27533 assert_eq!(
27534 workspace.panes().len(),
27535 1,
27536 "Should have one pane with one editor"
27537 );
27538 workspace.move_item_to_pane_in_direction(
27539 &MoveItemToPaneInDirection {
27540 direction: SplitDirection::Right,
27541 focus: false,
27542 clone: true,
27543 },
27544 window,
27545 cx,
27546 );
27547 })
27548 .unwrap();
27549 cx.run_until_parked();
27550 workspace
27551 .update(cx, |workspace, _, cx| {
27552 let panes = workspace.panes();
27553 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
27554 for pane in panes {
27555 let editor = pane
27556 .read(cx)
27557 .active_item()
27558 .and_then(|item| item.downcast::<Editor>())
27559 .expect("Should have opened an editor in each split");
27560 let editor_file = editor
27561 .read(cx)
27562 .buffer()
27563 .read(cx)
27564 .as_singleton()
27565 .expect("test deals with singleton buffers")
27566 .read(cx)
27567 .file()
27568 .expect("test buffese should have a file")
27569 .path();
27570 assert_eq!(
27571 editor_file.as_ref(),
27572 rel_path("first.rs"),
27573 "Both editors should be opened for the same file"
27574 )
27575 }
27576 })
27577 .unwrap();
27578
27579 cx.executor().advance_clock(Duration::from_millis(500));
27580 let save = editor.update_in(cx, |editor, window, cx| {
27581 editor.move_to_end(&MoveToEnd, window, cx);
27582 editor.handle_input("dirty", window, cx);
27583 editor.save(
27584 SaveOptions {
27585 format: true,
27586 autosave: true,
27587 },
27588 project.clone(),
27589 window,
27590 cx,
27591 )
27592 });
27593 save.await.unwrap();
27594
27595 color_request_handle.next().await.unwrap();
27596 cx.run_until_parked();
27597 assert_eq!(
27598 2,
27599 requests_made.load(atomic::Ordering::Acquire),
27600 "Should query for colors once per save (deduplicated) and once per formatting after save"
27601 );
27602
27603 drop(editor);
27604 let close = workspace
27605 .update(cx, |workspace, window, cx| {
27606 workspace.active_pane().update(cx, |pane, cx| {
27607 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27608 })
27609 })
27610 .unwrap();
27611 close.await.unwrap();
27612 let close = workspace
27613 .update(cx, |workspace, window, cx| {
27614 workspace.active_pane().update(cx, |pane, cx| {
27615 pane.close_active_item(&CloseActiveItem::default(), window, cx)
27616 })
27617 })
27618 .unwrap();
27619 close.await.unwrap();
27620 assert_eq!(
27621 2,
27622 requests_made.load(atomic::Ordering::Acquire),
27623 "After saving and closing all editors, no extra requests should be made"
27624 );
27625 workspace
27626 .update(cx, |workspace, _, cx| {
27627 assert!(
27628 workspace.active_item(cx).is_none(),
27629 "Should close all editors"
27630 )
27631 })
27632 .unwrap();
27633
27634 workspace
27635 .update(cx, |workspace, window, cx| {
27636 workspace.active_pane().update(cx, |pane, cx| {
27637 pane.navigate_backward(&workspace::GoBack, window, cx);
27638 })
27639 })
27640 .unwrap();
27641 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27642 cx.run_until_parked();
27643 let editor = workspace
27644 .update(cx, |workspace, _, cx| {
27645 workspace
27646 .active_item(cx)
27647 .expect("Should have reopened the editor again after navigating back")
27648 .downcast::<Editor>()
27649 .expect("Should be an editor")
27650 })
27651 .unwrap();
27652
27653 assert_eq!(
27654 2,
27655 requests_made.load(atomic::Ordering::Acquire),
27656 "Cache should be reused on buffer close and reopen"
27657 );
27658 editor.update(cx, |editor, cx| {
27659 assert_eq!(
27660 vec![expected_color],
27661 extract_color_inlays(editor, cx),
27662 "Should have an initial inlay"
27663 );
27664 });
27665
27666 drop(color_request_handle);
27667 let closure_requests_made = Arc::clone(&requests_made);
27668 let mut empty_color_request_handle = fake_language_server
27669 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
27670 let requests_made = Arc::clone(&closure_requests_made);
27671 async move {
27672 assert_eq!(
27673 params.text_document.uri,
27674 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27675 );
27676 requests_made.fetch_add(1, atomic::Ordering::Release);
27677 Ok(Vec::new())
27678 }
27679 });
27680 let save = editor.update_in(cx, |editor, window, cx| {
27681 editor.move_to_end(&MoveToEnd, window, cx);
27682 editor.handle_input("dirty_again", window, cx);
27683 editor.save(
27684 SaveOptions {
27685 format: false,
27686 autosave: true,
27687 },
27688 project.clone(),
27689 window,
27690 cx,
27691 )
27692 });
27693 save.await.unwrap();
27694
27695 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
27696 empty_color_request_handle.next().await.unwrap();
27697 cx.run_until_parked();
27698 assert_eq!(
27699 3,
27700 requests_made.load(atomic::Ordering::Acquire),
27701 "Should query for colors once per save only, as formatting was not requested"
27702 );
27703 editor.update(cx, |editor, cx| {
27704 assert_eq!(
27705 Vec::<Rgba>::new(),
27706 extract_color_inlays(editor, cx),
27707 "Should clear all colors when the server returns an empty response"
27708 );
27709 });
27710}
27711
27712#[gpui::test]
27713async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
27714 init_test(cx, |_| {});
27715 let (editor, cx) = cx.add_window_view(Editor::single_line);
27716 editor.update_in(cx, |editor, window, cx| {
27717 editor.set_text("oops\n\nwow\n", window, cx)
27718 });
27719 cx.run_until_parked();
27720 editor.update(cx, |editor, cx| {
27721 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
27722 });
27723 editor.update(cx, |editor, cx| {
27724 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
27725 });
27726 cx.run_until_parked();
27727 editor.update(cx, |editor, cx| {
27728 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
27729 });
27730}
27731
27732#[gpui::test]
27733async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
27734 init_test(cx, |_| {});
27735
27736 cx.update(|cx| {
27737 register_project_item::<Editor>(cx);
27738 });
27739
27740 let fs = FakeFs::new(cx.executor());
27741 fs.insert_tree("/root1", json!({})).await;
27742 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
27743 .await;
27744
27745 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
27746 let (workspace, cx) =
27747 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
27748
27749 let worktree_id = project.update(cx, |project, cx| {
27750 project.worktrees(cx).next().unwrap().read(cx).id()
27751 });
27752
27753 let handle = workspace
27754 .update_in(cx, |workspace, window, cx| {
27755 let project_path = (worktree_id, rel_path("one.pdf"));
27756 workspace.open_path(project_path, None, true, window, cx)
27757 })
27758 .await
27759 .unwrap();
27760 // The test file content `vec![0xff, 0xfe, ...]` starts with a UTF-16 LE BOM.
27761 // Previously, this fell back to `InvalidItemView` because it wasn't valid UTF-8.
27762 // With auto-detection enabled, this is now recognized as UTF-16 and opens in the Editor.
27763 assert_eq!(handle.to_any_view().entity_type(), TypeId::of::<Editor>());
27764}
27765
27766#[gpui::test]
27767async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
27768 init_test(cx, |_| {});
27769
27770 let language = Arc::new(Language::new(
27771 LanguageConfig::default(),
27772 Some(tree_sitter_rust::LANGUAGE.into()),
27773 ));
27774
27775 // Test hierarchical sibling navigation
27776 let text = r#"
27777 fn outer() {
27778 if condition {
27779 let a = 1;
27780 }
27781 let b = 2;
27782 }
27783
27784 fn another() {
27785 let c = 3;
27786 }
27787 "#;
27788
27789 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
27790 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
27791 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
27792
27793 // Wait for parsing to complete
27794 editor
27795 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
27796 .await;
27797
27798 editor.update_in(cx, |editor, window, cx| {
27799 // Start by selecting "let a = 1;" inside the if block
27800 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27801 s.select_display_ranges([
27802 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
27803 ]);
27804 });
27805
27806 let initial_selection = editor
27807 .selections
27808 .display_ranges(&editor.display_snapshot(cx));
27809 assert_eq!(initial_selection.len(), 1, "Should have one selection");
27810
27811 // Test select next sibling - should move up levels to find the next sibling
27812 // Since "let a = 1;" has no siblings in the if block, it should move up
27813 // to find "let b = 2;" which is a sibling of the if block
27814 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27815 let next_selection = editor
27816 .selections
27817 .display_ranges(&editor.display_snapshot(cx));
27818
27819 // Should have a selection and it should be different from the initial
27820 assert_eq!(
27821 next_selection.len(),
27822 1,
27823 "Should have one selection after next"
27824 );
27825 assert_ne!(
27826 next_selection[0], initial_selection[0],
27827 "Next sibling selection should be different"
27828 );
27829
27830 // Test hierarchical navigation by going to the end of the current function
27831 // and trying to navigate to the next function
27832 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27833 s.select_display_ranges([
27834 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
27835 ]);
27836 });
27837
27838 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
27839 let function_next_selection = editor
27840 .selections
27841 .display_ranges(&editor.display_snapshot(cx));
27842
27843 // Should move to the next function
27844 assert_eq!(
27845 function_next_selection.len(),
27846 1,
27847 "Should have one selection after function next"
27848 );
27849
27850 // Test select previous sibling navigation
27851 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
27852 let prev_selection = editor
27853 .selections
27854 .display_ranges(&editor.display_snapshot(cx));
27855
27856 // Should have a selection and it should be different
27857 assert_eq!(
27858 prev_selection.len(),
27859 1,
27860 "Should have one selection after prev"
27861 );
27862 assert_ne!(
27863 prev_selection[0], function_next_selection[0],
27864 "Previous sibling selection should be different from next"
27865 );
27866 });
27867}
27868
27869#[gpui::test]
27870async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
27871 init_test(cx, |_| {});
27872
27873 let mut cx = EditorTestContext::new(cx).await;
27874 cx.set_state(
27875 "let ˇvariable = 42;
27876let another = variable + 1;
27877let result = variable * 2;",
27878 );
27879
27880 // Set up document highlights manually (simulating LSP response)
27881 cx.update_editor(|editor, _window, cx| {
27882 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
27883
27884 // Create highlights for "variable" occurrences
27885 let highlight_ranges = [
27886 Point::new(0, 4)..Point::new(0, 12), // First "variable"
27887 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
27888 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
27889 ];
27890
27891 let anchor_ranges: Vec<_> = highlight_ranges
27892 .iter()
27893 .map(|range| range.clone().to_anchors(&buffer_snapshot))
27894 .collect();
27895
27896 editor.highlight_background::<DocumentHighlightRead>(
27897 &anchor_ranges,
27898 |_, theme| theme.colors().editor_document_highlight_read_background,
27899 cx,
27900 );
27901 });
27902
27903 // Go to next highlight - should move to second "variable"
27904 cx.update_editor(|editor, window, cx| {
27905 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27906 });
27907 cx.assert_editor_state(
27908 "let variable = 42;
27909let another = ˇvariable + 1;
27910let result = variable * 2;",
27911 );
27912
27913 // Go to next highlight - should move to third "variable"
27914 cx.update_editor(|editor, window, cx| {
27915 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27916 });
27917 cx.assert_editor_state(
27918 "let variable = 42;
27919let another = variable + 1;
27920let result = ˇvariable * 2;",
27921 );
27922
27923 // Go to next highlight - should stay at third "variable" (no wrap-around)
27924 cx.update_editor(|editor, window, cx| {
27925 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
27926 });
27927 cx.assert_editor_state(
27928 "let variable = 42;
27929let another = variable + 1;
27930let result = ˇvariable * 2;",
27931 );
27932
27933 // Now test going backwards from third position
27934 cx.update_editor(|editor, window, cx| {
27935 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27936 });
27937 cx.assert_editor_state(
27938 "let variable = 42;
27939let another = ˇvariable + 1;
27940let result = variable * 2;",
27941 );
27942
27943 // Go to previous highlight - should move to first "variable"
27944 cx.update_editor(|editor, window, cx| {
27945 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27946 });
27947 cx.assert_editor_state(
27948 "let ˇvariable = 42;
27949let another = variable + 1;
27950let result = variable * 2;",
27951 );
27952
27953 // Go to previous highlight - should stay on first "variable"
27954 cx.update_editor(|editor, window, cx| {
27955 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
27956 });
27957 cx.assert_editor_state(
27958 "let ˇvariable = 42;
27959let another = variable + 1;
27960let result = variable * 2;",
27961 );
27962}
27963
27964#[gpui::test]
27965async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
27966 cx: &mut gpui::TestAppContext,
27967) {
27968 init_test(cx, |_| {});
27969
27970 let url = "https://zed.dev";
27971
27972 let markdown_language = Arc::new(Language::new(
27973 LanguageConfig {
27974 name: "Markdown".into(),
27975 ..LanguageConfig::default()
27976 },
27977 None,
27978 ));
27979
27980 let mut cx = EditorTestContext::new(cx).await;
27981 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
27982 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
27983
27984 cx.update_editor(|editor, window, cx| {
27985 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
27986 editor.paste(&Paste, window, cx);
27987 });
27988
27989 cx.assert_editor_state(&format!(
27990 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
27991 ));
27992}
27993
27994#[gpui::test]
27995async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
27996 init_test(cx, |_| {});
27997
27998 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
27999 let mut cx = EditorTestContext::new(cx).await;
28000
28001 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28002
28003 // Case 1: Test if adding a character with multi cursors preserves nested list indents
28004 cx.set_state(&indoc! {"
28005 - [ ] Item 1
28006 - [ ] Item 1.a
28007 - [ˇ] Item 2
28008 - [ˇ] Item 2.a
28009 - [ˇ] Item 2.b
28010 "
28011 });
28012 cx.update_editor(|editor, window, cx| {
28013 editor.handle_input("x", window, cx);
28014 });
28015 cx.run_until_parked();
28016 cx.assert_editor_state(indoc! {"
28017 - [ ] Item 1
28018 - [ ] Item 1.a
28019 - [xˇ] Item 2
28020 - [xˇ] Item 2.a
28021 - [xˇ] Item 2.b
28022 "
28023 });
28024
28025 // Case 2: Test adding new line after nested list continues the list with unchecked task
28026 cx.set_state(&indoc! {"
28027 - [ ] Item 1
28028 - [ ] Item 1.a
28029 - [x] Item 2
28030 - [x] Item 2.a
28031 - [x] Item 2.bˇ"
28032 });
28033 cx.update_editor(|editor, window, cx| {
28034 editor.newline(&Newline, window, cx);
28035 });
28036 cx.assert_editor_state(indoc! {"
28037 - [ ] Item 1
28038 - [ ] Item 1.a
28039 - [x] Item 2
28040 - [x] Item 2.a
28041 - [x] Item 2.b
28042 - [ ] ˇ"
28043 });
28044
28045 // Case 3: Test adding content to continued list item
28046 cx.update_editor(|editor, window, cx| {
28047 editor.handle_input("Item 2.c", window, cx);
28048 });
28049 cx.run_until_parked();
28050 cx.assert_editor_state(indoc! {"
28051 - [ ] Item 1
28052 - [ ] Item 1.a
28053 - [x] Item 2
28054 - [x] Item 2.a
28055 - [x] Item 2.b
28056 - [ ] Item 2.cˇ"
28057 });
28058
28059 // Case 4: Test adding new line after nested ordered list continues with next number
28060 cx.set_state(indoc! {"
28061 1. Item 1
28062 1. Item 1.a
28063 2. Item 2
28064 1. Item 2.a
28065 2. Item 2.bˇ"
28066 });
28067 cx.update_editor(|editor, window, cx| {
28068 editor.newline(&Newline, window, cx);
28069 });
28070 cx.assert_editor_state(indoc! {"
28071 1. Item 1
28072 1. Item 1.a
28073 2. Item 2
28074 1. Item 2.a
28075 2. Item 2.b
28076 3. ˇ"
28077 });
28078
28079 // Case 5: Adding content to continued ordered list item
28080 cx.update_editor(|editor, window, cx| {
28081 editor.handle_input("Item 2.c", window, cx);
28082 });
28083 cx.run_until_parked();
28084 cx.assert_editor_state(indoc! {"
28085 1. Item 1
28086 1. Item 1.a
28087 2. Item 2
28088 1. Item 2.a
28089 2. Item 2.b
28090 3. Item 2.cˇ"
28091 });
28092
28093 // Case 6: Test adding new line after nested ordered list preserves indent of previous line
28094 cx.set_state(indoc! {"
28095 - Item 1
28096 - Item 1.a
28097 - Item 1.a
28098 ˇ"});
28099 cx.update_editor(|editor, window, cx| {
28100 editor.handle_input("-", window, cx);
28101 });
28102 cx.run_until_parked();
28103 cx.assert_editor_state(indoc! {"
28104 - Item 1
28105 - Item 1.a
28106 - Item 1.a
28107 -ˇ"});
28108
28109 // Case 7: Test blockquote newline preserves something
28110 cx.set_state(indoc! {"
28111 > Item 1ˇ"
28112 });
28113 cx.update_editor(|editor, window, cx| {
28114 editor.newline(&Newline, window, cx);
28115 });
28116 cx.assert_editor_state(indoc! {"
28117 > Item 1
28118 ˇ"
28119 });
28120}
28121
28122#[gpui::test]
28123async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
28124 cx: &mut gpui::TestAppContext,
28125) {
28126 init_test(cx, |_| {});
28127
28128 let url = "https://zed.dev";
28129
28130 let markdown_language = Arc::new(Language::new(
28131 LanguageConfig {
28132 name: "Markdown".into(),
28133 ..LanguageConfig::default()
28134 },
28135 None,
28136 ));
28137
28138 let mut cx = EditorTestContext::new(cx).await;
28139 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28140 cx.set_state(&format!(
28141 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
28142 ));
28143
28144 cx.update_editor(|editor, window, cx| {
28145 editor.copy(&Copy, window, cx);
28146 });
28147
28148 cx.set_state(&format!(
28149 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
28150 ));
28151
28152 cx.update_editor(|editor, window, cx| {
28153 editor.paste(&Paste, window, cx);
28154 });
28155
28156 cx.assert_editor_state(&format!(
28157 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
28158 ));
28159}
28160
28161#[gpui::test]
28162async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
28163 cx: &mut gpui::TestAppContext,
28164) {
28165 init_test(cx, |_| {});
28166
28167 let url = "https://zed.dev";
28168
28169 let markdown_language = Arc::new(Language::new(
28170 LanguageConfig {
28171 name: "Markdown".into(),
28172 ..LanguageConfig::default()
28173 },
28174 None,
28175 ));
28176
28177 let mut cx = EditorTestContext::new(cx).await;
28178 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28179 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
28180
28181 cx.update_editor(|editor, window, cx| {
28182 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28183 editor.paste(&Paste, window, cx);
28184 });
28185
28186 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
28187}
28188
28189#[gpui::test]
28190async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
28191 cx: &mut gpui::TestAppContext,
28192) {
28193 init_test(cx, |_| {});
28194
28195 let text = "Awesome";
28196
28197 let markdown_language = Arc::new(Language::new(
28198 LanguageConfig {
28199 name: "Markdown".into(),
28200 ..LanguageConfig::default()
28201 },
28202 None,
28203 ));
28204
28205 let mut cx = EditorTestContext::new(cx).await;
28206 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28207 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
28208
28209 cx.update_editor(|editor, window, cx| {
28210 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
28211 editor.paste(&Paste, window, cx);
28212 });
28213
28214 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
28215}
28216
28217#[gpui::test]
28218async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
28219 cx: &mut gpui::TestAppContext,
28220) {
28221 init_test(cx, |_| {});
28222
28223 let url = "https://zed.dev";
28224
28225 let markdown_language = Arc::new(Language::new(
28226 LanguageConfig {
28227 name: "Rust".into(),
28228 ..LanguageConfig::default()
28229 },
28230 None,
28231 ));
28232
28233 let mut cx = EditorTestContext::new(cx).await;
28234 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28235 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
28236
28237 cx.update_editor(|editor, window, cx| {
28238 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28239 editor.paste(&Paste, window, cx);
28240 });
28241
28242 cx.assert_editor_state(&format!(
28243 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
28244 ));
28245}
28246
28247#[gpui::test]
28248async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
28249 cx: &mut TestAppContext,
28250) {
28251 init_test(cx, |_| {});
28252
28253 let url = "https://zed.dev";
28254
28255 let markdown_language = Arc::new(Language::new(
28256 LanguageConfig {
28257 name: "Markdown".into(),
28258 ..LanguageConfig::default()
28259 },
28260 None,
28261 ));
28262
28263 let (editor, cx) = cx.add_window_view(|window, cx| {
28264 let multi_buffer = MultiBuffer::build_multi(
28265 [
28266 ("this will embed -> link", vec![Point::row_range(0..1)]),
28267 ("this will replace -> link", vec![Point::row_range(0..1)]),
28268 ],
28269 cx,
28270 );
28271 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
28272 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28273 s.select_ranges(vec![
28274 Point::new(0, 19)..Point::new(0, 23),
28275 Point::new(1, 21)..Point::new(1, 25),
28276 ])
28277 });
28278 let first_buffer_id = multi_buffer
28279 .read(cx)
28280 .excerpt_buffer_ids()
28281 .into_iter()
28282 .next()
28283 .unwrap();
28284 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
28285 first_buffer.update(cx, |buffer, cx| {
28286 buffer.set_language(Some(markdown_language.clone()), cx);
28287 });
28288
28289 editor
28290 });
28291 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28292
28293 cx.update_editor(|editor, window, cx| {
28294 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28295 editor.paste(&Paste, window, cx);
28296 });
28297
28298 cx.assert_editor_state(&format!(
28299 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
28300 ));
28301}
28302
28303#[gpui::test]
28304async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
28305 init_test(cx, |_| {});
28306
28307 let fs = FakeFs::new(cx.executor());
28308 fs.insert_tree(
28309 path!("/project"),
28310 json!({
28311 "first.rs": "# First Document\nSome content here.",
28312 "second.rs": "Plain text content for second file.",
28313 }),
28314 )
28315 .await;
28316
28317 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
28318 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
28319 let cx = &mut VisualTestContext::from_window(*workspace, cx);
28320
28321 let language = rust_lang();
28322 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
28323 language_registry.add(language.clone());
28324 let mut fake_servers = language_registry.register_fake_lsp(
28325 "Rust",
28326 FakeLspAdapter {
28327 ..FakeLspAdapter::default()
28328 },
28329 );
28330
28331 let buffer1 = project
28332 .update(cx, |project, cx| {
28333 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
28334 })
28335 .await
28336 .unwrap();
28337 let buffer2 = project
28338 .update(cx, |project, cx| {
28339 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
28340 })
28341 .await
28342 .unwrap();
28343
28344 let multi_buffer = cx.new(|cx| {
28345 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
28346 multi_buffer.set_excerpts_for_path(
28347 PathKey::for_buffer(&buffer1, cx),
28348 buffer1.clone(),
28349 [Point::zero()..buffer1.read(cx).max_point()],
28350 3,
28351 cx,
28352 );
28353 multi_buffer.set_excerpts_for_path(
28354 PathKey::for_buffer(&buffer2, cx),
28355 buffer2.clone(),
28356 [Point::zero()..buffer1.read(cx).max_point()],
28357 3,
28358 cx,
28359 );
28360 multi_buffer
28361 });
28362
28363 let (editor, cx) = cx.add_window_view(|window, cx| {
28364 Editor::new(
28365 EditorMode::full(),
28366 multi_buffer,
28367 Some(project.clone()),
28368 window,
28369 cx,
28370 )
28371 });
28372
28373 let fake_language_server = fake_servers.next().await.unwrap();
28374
28375 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
28376
28377 let save = editor.update_in(cx, |editor, window, cx| {
28378 assert!(editor.is_dirty(cx));
28379
28380 editor.save(
28381 SaveOptions {
28382 format: true,
28383 autosave: true,
28384 },
28385 project,
28386 window,
28387 cx,
28388 )
28389 });
28390 let (start_edit_tx, start_edit_rx) = oneshot::channel();
28391 let (done_edit_tx, done_edit_rx) = oneshot::channel();
28392 let mut done_edit_rx = Some(done_edit_rx);
28393 let mut start_edit_tx = Some(start_edit_tx);
28394
28395 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
28396 start_edit_tx.take().unwrap().send(()).unwrap();
28397 let done_edit_rx = done_edit_rx.take().unwrap();
28398 async move {
28399 done_edit_rx.await.unwrap();
28400 Ok(None)
28401 }
28402 });
28403
28404 start_edit_rx.await.unwrap();
28405 buffer2
28406 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
28407 .unwrap();
28408
28409 done_edit_tx.send(()).unwrap();
28410
28411 save.await.unwrap();
28412 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
28413}
28414
28415#[track_caller]
28416fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
28417 editor
28418 .all_inlays(cx)
28419 .into_iter()
28420 .filter_map(|inlay| inlay.get_color())
28421 .map(Rgba::from)
28422 .collect()
28423}
28424
28425#[gpui::test]
28426fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
28427 init_test(cx, |_| {});
28428
28429 let editor = cx.add_window(|window, cx| {
28430 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
28431 build_editor(buffer, window, cx)
28432 });
28433
28434 editor
28435 .update(cx, |editor, window, cx| {
28436 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28437 s.select_display_ranges([
28438 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
28439 ])
28440 });
28441
28442 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
28443
28444 assert_eq!(
28445 editor.display_text(cx),
28446 "line1\nline2\nline2",
28447 "Duplicating last line upward should create duplicate above, not on same line"
28448 );
28449
28450 assert_eq!(
28451 editor
28452 .selections
28453 .display_ranges(&editor.display_snapshot(cx)),
28454 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
28455 "Selection should move to the duplicated line"
28456 );
28457 })
28458 .unwrap();
28459}
28460
28461#[gpui::test]
28462async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
28463 init_test(cx, |_| {});
28464
28465 let mut cx = EditorTestContext::new(cx).await;
28466
28467 cx.set_state("line1\nline2ˇ");
28468
28469 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28470
28471 let clipboard_text = cx
28472 .read_from_clipboard()
28473 .and_then(|item| item.text().as_deref().map(str::to_string));
28474
28475 assert_eq!(
28476 clipboard_text,
28477 Some("line2\n".to_string()),
28478 "Copying a line without trailing newline should include a newline"
28479 );
28480
28481 cx.set_state("line1\nˇ");
28482
28483 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28484
28485 cx.assert_editor_state("line1\nline2\nˇ");
28486}
28487
28488#[gpui::test]
28489async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28490 init_test(cx, |_| {});
28491
28492 let mut cx = EditorTestContext::new(cx).await;
28493
28494 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28495
28496 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28497
28498 let clipboard_text = cx
28499 .read_from_clipboard()
28500 .and_then(|item| item.text().as_deref().map(str::to_string));
28501
28502 assert_eq!(
28503 clipboard_text,
28504 Some("line1\nline2\nline3\n".to_string()),
28505 "Copying multiple lines should include a single newline between lines"
28506 );
28507
28508 cx.set_state("lineA\nˇ");
28509
28510 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28511
28512 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28513}
28514
28515#[gpui::test]
28516async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28517 init_test(cx, |_| {});
28518
28519 let mut cx = EditorTestContext::new(cx).await;
28520
28521 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28522
28523 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
28524
28525 let clipboard_text = cx
28526 .read_from_clipboard()
28527 .and_then(|item| item.text().as_deref().map(str::to_string));
28528
28529 assert_eq!(
28530 clipboard_text,
28531 Some("line1\nline2\nline3\n".to_string()),
28532 "Copying multiple lines should include a single newline between lines"
28533 );
28534
28535 cx.set_state("lineA\nˇ");
28536
28537 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28538
28539 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28540}
28541
28542#[gpui::test]
28543async fn test_end_of_editor_context(cx: &mut TestAppContext) {
28544 init_test(cx, |_| {});
28545
28546 let mut cx = EditorTestContext::new(cx).await;
28547
28548 cx.set_state("line1\nline2ˇ");
28549 cx.update_editor(|e, window, cx| {
28550 e.set_mode(EditorMode::SingleLine);
28551 assert!(e.key_context(window, cx).contains("end_of_input"));
28552 });
28553 cx.set_state("ˇline1\nline2");
28554 cx.update_editor(|e, window, cx| {
28555 assert!(!e.key_context(window, cx).contains("end_of_input"));
28556 });
28557 cx.set_state("line1ˇ\nline2");
28558 cx.update_editor(|e, window, cx| {
28559 assert!(!e.key_context(window, cx).contains("end_of_input"));
28560 });
28561}
28562
28563#[gpui::test]
28564async fn test_sticky_scroll(cx: &mut TestAppContext) {
28565 init_test(cx, |_| {});
28566 let mut cx = EditorTestContext::new(cx).await;
28567
28568 let buffer = indoc! {"
28569 ˇfn foo() {
28570 let abc = 123;
28571 }
28572 struct Bar;
28573 impl Bar {
28574 fn new() -> Self {
28575 Self
28576 }
28577 }
28578 fn baz() {
28579 }
28580 "};
28581 cx.set_state(&buffer);
28582
28583 cx.update_editor(|e, _, cx| {
28584 e.buffer()
28585 .read(cx)
28586 .as_singleton()
28587 .unwrap()
28588 .update(cx, |buffer, cx| {
28589 buffer.set_language(Some(rust_lang()), cx);
28590 })
28591 });
28592
28593 let mut sticky_headers = |offset: ScrollOffset| {
28594 cx.update_editor(|e, window, cx| {
28595 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
28596 let style = e.style(cx).clone();
28597 EditorElement::sticky_headers(&e, &e.snapshot(window, cx), &style, cx)
28598 .into_iter()
28599 .map(
28600 |StickyHeader {
28601 start_point,
28602 offset,
28603 ..
28604 }| { (start_point, offset) },
28605 )
28606 .collect::<Vec<_>>()
28607 })
28608 };
28609
28610 let fn_foo = Point { row: 0, column: 0 };
28611 let impl_bar = Point { row: 4, column: 0 };
28612 let fn_new = Point { row: 5, column: 4 };
28613
28614 assert_eq!(sticky_headers(0.0), vec![]);
28615 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
28616 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
28617 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
28618 assert_eq!(sticky_headers(2.0), vec![]);
28619 assert_eq!(sticky_headers(2.5), vec![]);
28620 assert_eq!(sticky_headers(3.0), vec![]);
28621 assert_eq!(sticky_headers(3.5), vec![]);
28622 assert_eq!(sticky_headers(4.0), vec![]);
28623 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
28624 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
28625 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
28626 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
28627 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
28628 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
28629 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
28630 assert_eq!(sticky_headers(8.0), vec![]);
28631 assert_eq!(sticky_headers(8.5), vec![]);
28632 assert_eq!(sticky_headers(9.0), vec![]);
28633 assert_eq!(sticky_headers(9.5), vec![]);
28634 assert_eq!(sticky_headers(10.0), vec![]);
28635}
28636
28637#[gpui::test]
28638fn test_relative_line_numbers(cx: &mut TestAppContext) {
28639 init_test(cx, |_| {});
28640
28641 let buffer_1 = cx.new(|cx| Buffer::local("aaaaaaaaaa\nbbb\n", cx));
28642 let buffer_2 = cx.new(|cx| Buffer::local("cccccccccc\nddd\n", cx));
28643 let buffer_3 = cx.new(|cx| Buffer::local("eee\nffffffffff\n", cx));
28644
28645 let multibuffer = cx.new(|cx| {
28646 let mut multibuffer = MultiBuffer::new(ReadWrite);
28647 multibuffer.push_excerpts(
28648 buffer_1.clone(),
28649 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
28650 cx,
28651 );
28652 multibuffer.push_excerpts(
28653 buffer_2.clone(),
28654 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
28655 cx,
28656 );
28657 multibuffer.push_excerpts(
28658 buffer_3.clone(),
28659 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
28660 cx,
28661 );
28662 multibuffer
28663 });
28664
28665 // wrapped contents of multibuffer:
28666 // aaa
28667 // aaa
28668 // aaa
28669 // a
28670 // bbb
28671 //
28672 // ccc
28673 // ccc
28674 // ccc
28675 // c
28676 // ddd
28677 //
28678 // eee
28679 // fff
28680 // fff
28681 // fff
28682 // f
28683
28684 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
28685 editor.update_in(cx, |editor, window, cx| {
28686 editor.set_wrap_width(Some(30.0.into()), cx); // every 3 characters
28687
28688 // includes trailing newlines.
28689 let expected_line_numbers = [2, 6, 7, 10, 14, 15, 18, 19, 23];
28690 let expected_wrapped_line_numbers = [
28691 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 18, 19, 20, 21, 22, 23,
28692 ];
28693
28694 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28695 s.select_ranges([
28696 Point::new(7, 0)..Point::new(7, 1), // second row of `ccc`
28697 ]);
28698 });
28699
28700 let snapshot = editor.snapshot(window, cx);
28701
28702 // these are all 0-indexed
28703 let base_display_row = DisplayRow(11);
28704 let base_row = 3;
28705 let wrapped_base_row = 7;
28706
28707 // test not counting wrapped lines
28708 let expected_relative_numbers = expected_line_numbers
28709 .into_iter()
28710 .enumerate()
28711 .map(|(i, row)| (DisplayRow(row), i.abs_diff(base_row) as u32))
28712 .collect_vec();
28713 let actual_relative_numbers = snapshot
28714 .calculate_relative_line_numbers(
28715 &(DisplayRow(0)..DisplayRow(24)),
28716 base_display_row,
28717 false,
28718 )
28719 .into_iter()
28720 .sorted()
28721 .collect_vec();
28722 assert_eq!(expected_relative_numbers, actual_relative_numbers);
28723 // check `calculate_relative_line_numbers()` against `relative_line_delta()` for each line
28724 for (display_row, relative_number) in expected_relative_numbers {
28725 assert_eq!(
28726 relative_number,
28727 snapshot
28728 .relative_line_delta(display_row, base_display_row)
28729 .unsigned_abs() as u32,
28730 );
28731 }
28732
28733 // test counting wrapped lines
28734 let expected_wrapped_relative_numbers = expected_wrapped_line_numbers
28735 .into_iter()
28736 .enumerate()
28737 .map(|(i, row)| (DisplayRow(row), i.abs_diff(wrapped_base_row) as u32))
28738 .collect_vec();
28739 let actual_relative_numbers = snapshot
28740 .calculate_relative_line_numbers(
28741 &(DisplayRow(0)..DisplayRow(24)),
28742 base_display_row,
28743 true,
28744 )
28745 .into_iter()
28746 .sorted()
28747 .collect_vec();
28748 assert_eq!(expected_wrapped_relative_numbers, actual_relative_numbers);
28749 // check `calculate_relative_line_numbers()` against `relative_wrapped_line_delta()` for each line
28750 for (display_row, relative_number) in expected_wrapped_relative_numbers {
28751 assert_eq!(
28752 relative_number,
28753 snapshot
28754 .relative_wrapped_line_delta(display_row, base_display_row)
28755 .unsigned_abs() as u32,
28756 );
28757 }
28758 });
28759}
28760
28761#[gpui::test]
28762async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
28763 init_test(cx, |_| {});
28764 cx.update(|cx| {
28765 SettingsStore::update_global(cx, |store, cx| {
28766 store.update_user_settings(cx, |settings| {
28767 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
28768 enabled: Some(true),
28769 })
28770 });
28771 });
28772 });
28773 let mut cx = EditorTestContext::new(cx).await;
28774
28775 let line_height = cx.update_editor(|editor, window, cx| {
28776 editor
28777 .style(cx)
28778 .text
28779 .line_height_in_pixels(window.rem_size())
28780 });
28781
28782 let buffer = indoc! {"
28783 ˇfn foo() {
28784 let abc = 123;
28785 }
28786 struct Bar;
28787 impl Bar {
28788 fn new() -> Self {
28789 Self
28790 }
28791 }
28792 fn baz() {
28793 }
28794 "};
28795 cx.set_state(&buffer);
28796
28797 cx.update_editor(|e, _, cx| {
28798 e.buffer()
28799 .read(cx)
28800 .as_singleton()
28801 .unwrap()
28802 .update(cx, |buffer, cx| {
28803 buffer.set_language(Some(rust_lang()), cx);
28804 })
28805 });
28806
28807 let fn_foo = || empty_range(0, 0);
28808 let impl_bar = || empty_range(4, 0);
28809 let fn_new = || empty_range(5, 4);
28810
28811 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
28812 cx.update_editor(|e, window, cx| {
28813 e.scroll(
28814 gpui::Point {
28815 x: 0.,
28816 y: scroll_offset,
28817 },
28818 None,
28819 window,
28820 cx,
28821 );
28822 });
28823 cx.simulate_click(
28824 gpui::Point {
28825 x: px(0.),
28826 y: click_offset as f32 * line_height,
28827 },
28828 Modifiers::none(),
28829 );
28830 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
28831 };
28832
28833 assert_eq!(
28834 scroll_and_click(
28835 4.5, // impl Bar is halfway off the screen
28836 0.0 // click top of screen
28837 ),
28838 // scrolled to impl Bar
28839 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28840 );
28841
28842 assert_eq!(
28843 scroll_and_click(
28844 4.5, // impl Bar is halfway off the screen
28845 0.25 // click middle of impl Bar
28846 ),
28847 // scrolled to impl Bar
28848 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28849 );
28850
28851 assert_eq!(
28852 scroll_and_click(
28853 4.5, // impl Bar is halfway off the screen
28854 1.5 // click below impl Bar (e.g. fn new())
28855 ),
28856 // scrolled to fn new() - this is below the impl Bar header which has persisted
28857 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
28858 );
28859
28860 assert_eq!(
28861 scroll_and_click(
28862 5.5, // fn new is halfway underneath impl Bar
28863 0.75 // click on the overlap of impl Bar and fn new()
28864 ),
28865 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
28866 );
28867
28868 assert_eq!(
28869 scroll_and_click(
28870 5.5, // fn new is halfway underneath impl Bar
28871 1.25 // click on the visible part of fn new()
28872 ),
28873 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
28874 );
28875
28876 assert_eq!(
28877 scroll_and_click(
28878 1.5, // fn foo is halfway off the screen
28879 0.0 // click top of screen
28880 ),
28881 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
28882 );
28883
28884 assert_eq!(
28885 scroll_and_click(
28886 1.5, // fn foo is halfway off the screen
28887 0.75 // click visible part of let abc...
28888 )
28889 .0,
28890 // no change in scroll
28891 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
28892 (gpui::Point { x: 0., y: 1.5 })
28893 );
28894}
28895
28896#[gpui::test]
28897async fn test_next_prev_reference(cx: &mut TestAppContext) {
28898 const CYCLE_POSITIONS: &[&'static str] = &[
28899 indoc! {"
28900 fn foo() {
28901 let ˇabc = 123;
28902 let x = abc + 1;
28903 let y = abc + 2;
28904 let z = abc + 2;
28905 }
28906 "},
28907 indoc! {"
28908 fn foo() {
28909 let abc = 123;
28910 let x = ˇabc + 1;
28911 let y = abc + 2;
28912 let z = abc + 2;
28913 }
28914 "},
28915 indoc! {"
28916 fn foo() {
28917 let abc = 123;
28918 let x = abc + 1;
28919 let y = ˇabc + 2;
28920 let z = abc + 2;
28921 }
28922 "},
28923 indoc! {"
28924 fn foo() {
28925 let abc = 123;
28926 let x = abc + 1;
28927 let y = abc + 2;
28928 let z = ˇabc + 2;
28929 }
28930 "},
28931 ];
28932
28933 init_test(cx, |_| {});
28934
28935 let mut cx = EditorLspTestContext::new_rust(
28936 lsp::ServerCapabilities {
28937 references_provider: Some(lsp::OneOf::Left(true)),
28938 ..Default::default()
28939 },
28940 cx,
28941 )
28942 .await;
28943
28944 // importantly, the cursor is in the middle
28945 cx.set_state(indoc! {"
28946 fn foo() {
28947 let aˇbc = 123;
28948 let x = abc + 1;
28949 let y = abc + 2;
28950 let z = abc + 2;
28951 }
28952 "});
28953
28954 let reference_ranges = [
28955 lsp::Position::new(1, 8),
28956 lsp::Position::new(2, 12),
28957 lsp::Position::new(3, 12),
28958 lsp::Position::new(4, 12),
28959 ]
28960 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
28961
28962 cx.lsp
28963 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
28964 Ok(Some(
28965 reference_ranges
28966 .map(|range| lsp::Location {
28967 uri: params.text_document_position.text_document.uri.clone(),
28968 range,
28969 })
28970 .to_vec(),
28971 ))
28972 });
28973
28974 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
28975 cx.update_editor(|editor, window, cx| {
28976 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
28977 })
28978 .unwrap()
28979 .await
28980 .unwrap()
28981 };
28982
28983 _move(Direction::Next, 1, &mut cx).await;
28984 cx.assert_editor_state(CYCLE_POSITIONS[1]);
28985
28986 _move(Direction::Next, 1, &mut cx).await;
28987 cx.assert_editor_state(CYCLE_POSITIONS[2]);
28988
28989 _move(Direction::Next, 1, &mut cx).await;
28990 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28991
28992 // loops back to the start
28993 _move(Direction::Next, 1, &mut cx).await;
28994 cx.assert_editor_state(CYCLE_POSITIONS[0]);
28995
28996 // loops back to the end
28997 _move(Direction::Prev, 1, &mut cx).await;
28998 cx.assert_editor_state(CYCLE_POSITIONS[3]);
28999
29000 _move(Direction::Prev, 1, &mut cx).await;
29001 cx.assert_editor_state(CYCLE_POSITIONS[2]);
29002
29003 _move(Direction::Prev, 1, &mut cx).await;
29004 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29005
29006 _move(Direction::Prev, 1, &mut cx).await;
29007 cx.assert_editor_state(CYCLE_POSITIONS[0]);
29008
29009 _move(Direction::Next, 3, &mut cx).await;
29010 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29011
29012 _move(Direction::Prev, 2, &mut cx).await;
29013 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29014}
29015
29016#[gpui::test]
29017async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
29018 init_test(cx, |_| {});
29019
29020 let (editor, cx) = cx.add_window_view(|window, cx| {
29021 let multi_buffer = MultiBuffer::build_multi(
29022 [
29023 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29024 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29025 ],
29026 cx,
29027 );
29028 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
29029 });
29030
29031 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29032 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
29033
29034 cx.assert_excerpts_with_selections(indoc! {"
29035 [EXCERPT]
29036 ˇ1
29037 2
29038 3
29039 [EXCERPT]
29040 1
29041 2
29042 3
29043 "});
29044
29045 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
29046 cx.update_editor(|editor, window, cx| {
29047 editor.change_selections(None.into(), window, cx, |s| {
29048 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29049 });
29050 });
29051 cx.assert_excerpts_with_selections(indoc! {"
29052 [EXCERPT]
29053 1
29054 2ˇ
29055 3
29056 [EXCERPT]
29057 1
29058 2
29059 3
29060 "});
29061
29062 cx.update_editor(|editor, window, cx| {
29063 editor
29064 .select_all_matches(&SelectAllMatches, window, cx)
29065 .unwrap();
29066 });
29067 cx.assert_excerpts_with_selections(indoc! {"
29068 [EXCERPT]
29069 1
29070 2ˇ
29071 3
29072 [EXCERPT]
29073 1
29074 2ˇ
29075 3
29076 "});
29077
29078 cx.update_editor(|editor, window, cx| {
29079 editor.handle_input("X", window, cx);
29080 });
29081 cx.assert_excerpts_with_selections(indoc! {"
29082 [EXCERPT]
29083 1
29084 Xˇ
29085 3
29086 [EXCERPT]
29087 1
29088 Xˇ
29089 3
29090 "});
29091
29092 // Scenario 2: Select "2", then fold second buffer before insertion
29093 cx.update_multibuffer(|mb, cx| {
29094 for buffer_id in buffer_ids.iter() {
29095 let buffer = mb.buffer(*buffer_id).unwrap();
29096 buffer.update(cx, |buffer, cx| {
29097 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
29098 });
29099 }
29100 });
29101
29102 // Select "2" and select all matches
29103 cx.update_editor(|editor, window, cx| {
29104 editor.change_selections(None.into(), window, cx, |s| {
29105 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29106 });
29107 editor
29108 .select_all_matches(&SelectAllMatches, window, cx)
29109 .unwrap();
29110 });
29111
29112 // Fold second buffer - should remove selections from folded buffer
29113 cx.update_editor(|editor, _, cx| {
29114 editor.fold_buffer(buffer_ids[1], cx);
29115 });
29116 cx.assert_excerpts_with_selections(indoc! {"
29117 [EXCERPT]
29118 1
29119 2ˇ
29120 3
29121 [EXCERPT]
29122 [FOLDED]
29123 "});
29124
29125 // Insert text - should only affect first buffer
29126 cx.update_editor(|editor, window, cx| {
29127 editor.handle_input("Y", window, cx);
29128 });
29129 cx.update_editor(|editor, _, cx| {
29130 editor.unfold_buffer(buffer_ids[1], cx);
29131 });
29132 cx.assert_excerpts_with_selections(indoc! {"
29133 [EXCERPT]
29134 1
29135 Yˇ
29136 3
29137 [EXCERPT]
29138 1
29139 2
29140 3
29141 "});
29142
29143 // Scenario 3: Select "2", then fold first buffer before insertion
29144 cx.update_multibuffer(|mb, cx| {
29145 for buffer_id in buffer_ids.iter() {
29146 let buffer = mb.buffer(*buffer_id).unwrap();
29147 buffer.update(cx, |buffer, cx| {
29148 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
29149 });
29150 }
29151 });
29152
29153 // Select "2" and select all matches
29154 cx.update_editor(|editor, window, cx| {
29155 editor.change_selections(None.into(), window, cx, |s| {
29156 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29157 });
29158 editor
29159 .select_all_matches(&SelectAllMatches, window, cx)
29160 .unwrap();
29161 });
29162
29163 // Fold first buffer - should remove selections from folded buffer
29164 cx.update_editor(|editor, _, cx| {
29165 editor.fold_buffer(buffer_ids[0], cx);
29166 });
29167 cx.assert_excerpts_with_selections(indoc! {"
29168 [EXCERPT]
29169 [FOLDED]
29170 [EXCERPT]
29171 1
29172 2ˇ
29173 3
29174 "});
29175
29176 // Insert text - should only affect second buffer
29177 cx.update_editor(|editor, window, cx| {
29178 editor.handle_input("Z", window, cx);
29179 });
29180 cx.update_editor(|editor, _, cx| {
29181 editor.unfold_buffer(buffer_ids[0], cx);
29182 });
29183 cx.assert_excerpts_with_selections(indoc! {"
29184 [EXCERPT]
29185 1
29186 2
29187 3
29188 [EXCERPT]
29189 1
29190 Zˇ
29191 3
29192 "});
29193
29194 // Test correct folded header is selected upon fold
29195 cx.update_editor(|editor, _, cx| {
29196 editor.fold_buffer(buffer_ids[0], cx);
29197 editor.fold_buffer(buffer_ids[1], cx);
29198 });
29199 cx.assert_excerpts_with_selections(indoc! {"
29200 [EXCERPT]
29201 [FOLDED]
29202 [EXCERPT]
29203 ˇ[FOLDED]
29204 "});
29205
29206 // Test selection inside folded buffer unfolds it on type
29207 cx.update_editor(|editor, window, cx| {
29208 editor.handle_input("W", window, cx);
29209 });
29210 cx.update_editor(|editor, _, cx| {
29211 editor.unfold_buffer(buffer_ids[0], cx);
29212 });
29213 cx.assert_excerpts_with_selections(indoc! {"
29214 [EXCERPT]
29215 1
29216 2
29217 3
29218 [EXCERPT]
29219 Wˇ1
29220 Z
29221 3
29222 "});
29223}
29224
29225#[gpui::test]
29226async fn test_filtered_editor_pair(cx: &mut gpui::TestAppContext) {
29227 init_test(cx, |_| {});
29228 let mut leader_cx = EditorTestContext::new(cx).await;
29229
29230 let diff_base = indoc!(
29231 r#"
29232 one
29233 two
29234 three
29235 four
29236 five
29237 six
29238 "#
29239 );
29240
29241 let initial_state = indoc!(
29242 r#"
29243 ˇone
29244 two
29245 THREE
29246 four
29247 five
29248 six
29249 "#
29250 );
29251
29252 leader_cx.set_state(initial_state);
29253
29254 leader_cx.set_head_text(&diff_base);
29255 leader_cx.run_until_parked();
29256
29257 let follower = leader_cx.update_multibuffer(|leader, cx| {
29258 leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
29259 leader.set_all_diff_hunks_expanded(cx);
29260 leader.get_or_create_follower(cx)
29261 });
29262 follower.update(cx, |follower, cx| {
29263 follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
29264 follower.set_all_diff_hunks_expanded(cx);
29265 });
29266
29267 let follower_editor =
29268 leader_cx.new_window_entity(|window, cx| build_editor(follower, window, cx));
29269 // leader_cx.window.focus(&follower_editor.focus_handle(cx));
29270
29271 let mut follower_cx = EditorTestContext::for_editor_in(follower_editor, &mut leader_cx).await;
29272 cx.run_until_parked();
29273
29274 leader_cx.assert_editor_state(initial_state);
29275 follower_cx.assert_editor_state(indoc! {
29276 r#"
29277 ˇone
29278 two
29279 three
29280 four
29281 five
29282 six
29283 "#
29284 });
29285
29286 follower_cx.editor(|editor, _window, cx| {
29287 assert!(editor.read_only(cx));
29288 });
29289
29290 leader_cx.update_editor(|editor, _window, cx| {
29291 editor.edit([(Point::new(4, 0)..Point::new(5, 0), "FIVE\n")], cx);
29292 });
29293 cx.run_until_parked();
29294
29295 leader_cx.assert_editor_state(indoc! {
29296 r#"
29297 ˇone
29298 two
29299 THREE
29300 four
29301 FIVE
29302 six
29303 "#
29304 });
29305
29306 follower_cx.assert_editor_state(indoc! {
29307 r#"
29308 ˇone
29309 two
29310 three
29311 four
29312 five
29313 six
29314 "#
29315 });
29316
29317 leader_cx.update_editor(|editor, _window, cx| {
29318 editor.edit([(Point::new(6, 0)..Point::new(6, 0), "SEVEN")], cx);
29319 });
29320 cx.run_until_parked();
29321
29322 leader_cx.assert_editor_state(indoc! {
29323 r#"
29324 ˇone
29325 two
29326 THREE
29327 four
29328 FIVE
29329 six
29330 SEVEN"#
29331 });
29332
29333 follower_cx.assert_editor_state(indoc! {
29334 r#"
29335 ˇone
29336 two
29337 three
29338 four
29339 five
29340 six
29341 "#
29342 });
29343
29344 leader_cx.update_editor(|editor, window, cx| {
29345 editor.move_down(&MoveDown, window, cx);
29346 editor.refresh_selected_text_highlights(true, window, cx);
29347 });
29348 leader_cx.run_until_parked();
29349}
29350
29351#[gpui::test]
29352async fn test_filtered_editor_pair_complex(cx: &mut gpui::TestAppContext) {
29353 init_test(cx, |_| {});
29354 let base_text = "base\n";
29355 let buffer_text = "buffer\n";
29356
29357 let buffer1 = cx.new(|cx| Buffer::local(buffer_text, cx));
29358 let diff1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer1, cx));
29359
29360 let extra_buffer_1 = cx.new(|cx| Buffer::local("dummy text 1\n", cx));
29361 let extra_diff_1 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_1, cx));
29362 let extra_buffer_2 = cx.new(|cx| Buffer::local("dummy text 2\n", cx));
29363 let extra_diff_2 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_2, cx));
29364
29365 let leader = cx.new(|cx| {
29366 let mut leader = MultiBuffer::new(Capability::ReadWrite);
29367 leader.set_all_diff_hunks_expanded(cx);
29368 leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
29369 leader
29370 });
29371 let follower = leader.update(cx, |leader, cx| leader.get_or_create_follower(cx));
29372 follower.update(cx, |follower, _| {
29373 follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
29374 });
29375
29376 leader.update(cx, |leader, cx| {
29377 leader.insert_excerpts_after(
29378 ExcerptId::min(),
29379 extra_buffer_2.clone(),
29380 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
29381 cx,
29382 );
29383 leader.add_diff(extra_diff_2.clone(), cx);
29384
29385 leader.insert_excerpts_after(
29386 ExcerptId::min(),
29387 extra_buffer_1.clone(),
29388 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
29389 cx,
29390 );
29391 leader.add_diff(extra_diff_1.clone(), cx);
29392
29393 leader.insert_excerpts_after(
29394 ExcerptId::min(),
29395 buffer1.clone(),
29396 vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
29397 cx,
29398 );
29399 leader.add_diff(diff1.clone(), cx);
29400 });
29401
29402 cx.run_until_parked();
29403 let mut cx = cx.add_empty_window();
29404
29405 let leader_editor = cx
29406 .new_window_entity(|window, cx| Editor::for_multibuffer(leader.clone(), None, window, cx));
29407 let follower_editor = cx.new_window_entity(|window, cx| {
29408 Editor::for_multibuffer(follower.clone(), None, window, cx)
29409 });
29410
29411 let mut leader_cx = EditorTestContext::for_editor_in(leader_editor.clone(), &mut cx).await;
29412 leader_cx.assert_editor_state(indoc! {"
29413 ˇbuffer
29414
29415 dummy text 1
29416
29417 dummy text 2
29418 "});
29419 let mut follower_cx = EditorTestContext::for_editor_in(follower_editor.clone(), &mut cx).await;
29420 follower_cx.assert_editor_state(indoc! {"
29421 ˇbase
29422
29423
29424 "});
29425}
29426
29427#[gpui::test]
29428async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
29429 init_test(cx, |_| {});
29430
29431 let (editor, cx) = cx.add_window_view(|window, cx| {
29432 let multi_buffer = MultiBuffer::build_multi(
29433 [
29434 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29435 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
29436 ],
29437 cx,
29438 );
29439 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
29440 });
29441
29442 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29443
29444 cx.assert_excerpts_with_selections(indoc! {"
29445 [EXCERPT]
29446 ˇ1
29447 2
29448 3
29449 [EXCERPT]
29450 1
29451 2
29452 3
29453 4
29454 5
29455 6
29456 7
29457 8
29458 9
29459 "});
29460
29461 cx.update_editor(|editor, window, cx| {
29462 editor.change_selections(None.into(), window, cx, |s| {
29463 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
29464 });
29465 });
29466
29467 cx.assert_excerpts_with_selections(indoc! {"
29468 [EXCERPT]
29469 1
29470 2
29471 3
29472 [EXCERPT]
29473 1
29474 2
29475 3
29476 4
29477 5
29478 6
29479 ˇ7
29480 8
29481 9
29482 "});
29483
29484 cx.update_editor(|editor, _window, cx| {
29485 editor.set_vertical_scroll_margin(0, cx);
29486 });
29487
29488 cx.update_editor(|editor, window, cx| {
29489 assert_eq!(editor.vertical_scroll_margin(), 0);
29490 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29491 assert_eq!(
29492 editor.snapshot(window, cx).scroll_position(),
29493 gpui::Point::new(0., 12.0)
29494 );
29495 });
29496
29497 cx.update_editor(|editor, _window, cx| {
29498 editor.set_vertical_scroll_margin(3, cx);
29499 });
29500
29501 cx.update_editor(|editor, window, cx| {
29502 assert_eq!(editor.vertical_scroll_margin(), 3);
29503 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29504 assert_eq!(
29505 editor.snapshot(window, cx).scroll_position(),
29506 gpui::Point::new(0., 9.0)
29507 );
29508 });
29509}
29510
29511#[gpui::test]
29512async fn test_find_references_single_case(cx: &mut TestAppContext) {
29513 init_test(cx, |_| {});
29514 let mut cx = EditorLspTestContext::new_rust(
29515 lsp::ServerCapabilities {
29516 references_provider: Some(lsp::OneOf::Left(true)),
29517 ..lsp::ServerCapabilities::default()
29518 },
29519 cx,
29520 )
29521 .await;
29522
29523 let before = indoc!(
29524 r#"
29525 fn main() {
29526 let aˇbc = 123;
29527 let xyz = abc;
29528 }
29529 "#
29530 );
29531 let after = indoc!(
29532 r#"
29533 fn main() {
29534 let abc = 123;
29535 let xyz = ˇabc;
29536 }
29537 "#
29538 );
29539
29540 cx.lsp
29541 .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
29542 Ok(Some(vec![
29543 lsp::Location {
29544 uri: params.text_document_position.text_document.uri.clone(),
29545 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 11)),
29546 },
29547 lsp::Location {
29548 uri: params.text_document_position.text_document.uri,
29549 range: lsp::Range::new(lsp::Position::new(2, 14), lsp::Position::new(2, 17)),
29550 },
29551 ]))
29552 });
29553
29554 cx.set_state(before);
29555
29556 let action = FindAllReferences {
29557 always_open_multibuffer: false,
29558 };
29559
29560 let navigated = cx
29561 .update_editor(|editor, window, cx| editor.find_all_references(&action, window, cx))
29562 .expect("should have spawned a task")
29563 .await
29564 .unwrap();
29565
29566 assert_eq!(navigated, Navigated::No);
29567
29568 cx.run_until_parked();
29569
29570 cx.assert_editor_state(after);
29571}
29572
29573#[gpui::test]
29574async fn test_newline_task_list_continuation(cx: &mut TestAppContext) {
29575 init_test(cx, |settings| {
29576 settings.defaults.tab_size = Some(2.try_into().unwrap());
29577 });
29578
29579 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29580 let mut cx = EditorTestContext::new(cx).await;
29581 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29582
29583 // Case 1: Adding newline after (whitespace + prefix + any non-whitespace) adds marker
29584 cx.set_state(indoc! {"
29585 - [ ] taskˇ
29586 "});
29587 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29588 cx.wait_for_autoindent_applied().await;
29589 cx.assert_editor_state(indoc! {"
29590 - [ ] task
29591 - [ ] ˇ
29592 "});
29593
29594 // Case 2: Works with checked task items too
29595 cx.set_state(indoc! {"
29596 - [x] completed taskˇ
29597 "});
29598 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29599 cx.wait_for_autoindent_applied().await;
29600 cx.assert_editor_state(indoc! {"
29601 - [x] completed task
29602 - [ ] ˇ
29603 "});
29604
29605 // Case 3: Cursor position doesn't matter - content after marker is what counts
29606 cx.set_state(indoc! {"
29607 - [ ] taˇsk
29608 "});
29609 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29610 cx.wait_for_autoindent_applied().await;
29611 cx.assert_editor_state(indoc! {"
29612 - [ ] ta
29613 - [ ] ˇsk
29614 "});
29615
29616 // Case 4: Adding newline after (whitespace + prefix + some whitespace) does NOT add marker
29617 cx.set_state(indoc! {"
29618 - [ ] ˇ
29619 "});
29620 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29621 cx.wait_for_autoindent_applied().await;
29622 cx.assert_editor_state(
29623 indoc! {"
29624 - [ ]$$
29625 ˇ
29626 "}
29627 .replace("$", " ")
29628 .as_str(),
29629 );
29630
29631 // Case 5: Adding newline with content adds marker preserving indentation
29632 cx.set_state(indoc! {"
29633 - [ ] task
29634 - [ ] indentedˇ
29635 "});
29636 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29637 cx.wait_for_autoindent_applied().await;
29638 cx.assert_editor_state(indoc! {"
29639 - [ ] task
29640 - [ ] indented
29641 - [ ] ˇ
29642 "});
29643
29644 // Case 6: Adding newline with cursor right after prefix, unindents
29645 cx.set_state(indoc! {"
29646 - [ ] task
29647 - [ ] sub task
29648 - [ ] ˇ
29649 "});
29650 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29651 cx.wait_for_autoindent_applied().await;
29652 cx.assert_editor_state(indoc! {"
29653 - [ ] task
29654 - [ ] sub task
29655 - [ ] ˇ
29656 "});
29657 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29658 cx.wait_for_autoindent_applied().await;
29659
29660 // Case 7: Adding newline with cursor right after prefix, removes marker
29661 cx.assert_editor_state(indoc! {"
29662 - [ ] task
29663 - [ ] sub task
29664 - [ ] ˇ
29665 "});
29666 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29667 cx.wait_for_autoindent_applied().await;
29668 cx.assert_editor_state(indoc! {"
29669 - [ ] task
29670 - [ ] sub task
29671 ˇ
29672 "});
29673
29674 // Case 8: Cursor before or inside prefix does not add marker
29675 cx.set_state(indoc! {"
29676 ˇ- [ ] task
29677 "});
29678 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29679 cx.wait_for_autoindent_applied().await;
29680 cx.assert_editor_state(indoc! {"
29681
29682 ˇ- [ ] task
29683 "});
29684
29685 cx.set_state(indoc! {"
29686 - [ˇ ] task
29687 "});
29688 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29689 cx.wait_for_autoindent_applied().await;
29690 cx.assert_editor_state(indoc! {"
29691 - [
29692 ˇ
29693 ] task
29694 "});
29695}
29696
29697#[gpui::test]
29698async fn test_newline_unordered_list_continuation(cx: &mut TestAppContext) {
29699 init_test(cx, |settings| {
29700 settings.defaults.tab_size = Some(2.try_into().unwrap());
29701 });
29702
29703 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29704 let mut cx = EditorTestContext::new(cx).await;
29705 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29706
29707 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) adds marker
29708 cx.set_state(indoc! {"
29709 - itemˇ
29710 "});
29711 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29712 cx.wait_for_autoindent_applied().await;
29713 cx.assert_editor_state(indoc! {"
29714 - item
29715 - ˇ
29716 "});
29717
29718 // Case 2: Works with different markers
29719 cx.set_state(indoc! {"
29720 * starred itemˇ
29721 "});
29722 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29723 cx.wait_for_autoindent_applied().await;
29724 cx.assert_editor_state(indoc! {"
29725 * starred item
29726 * ˇ
29727 "});
29728
29729 cx.set_state(indoc! {"
29730 + plus itemˇ
29731 "});
29732 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29733 cx.wait_for_autoindent_applied().await;
29734 cx.assert_editor_state(indoc! {"
29735 + plus item
29736 + ˇ
29737 "});
29738
29739 // Case 3: Cursor position doesn't matter - content after marker is what counts
29740 cx.set_state(indoc! {"
29741 - itˇem
29742 "});
29743 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29744 cx.wait_for_autoindent_applied().await;
29745 cx.assert_editor_state(indoc! {"
29746 - it
29747 - ˇem
29748 "});
29749
29750 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
29751 cx.set_state(indoc! {"
29752 - ˇ
29753 "});
29754 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29755 cx.wait_for_autoindent_applied().await;
29756 cx.assert_editor_state(
29757 indoc! {"
29758 - $
29759 ˇ
29760 "}
29761 .replace("$", " ")
29762 .as_str(),
29763 );
29764
29765 // Case 5: Adding newline with content adds marker preserving indentation
29766 cx.set_state(indoc! {"
29767 - item
29768 - indentedˇ
29769 "});
29770 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29771 cx.wait_for_autoindent_applied().await;
29772 cx.assert_editor_state(indoc! {"
29773 - item
29774 - indented
29775 - ˇ
29776 "});
29777
29778 // Case 6: Adding newline with cursor right after marker, unindents
29779 cx.set_state(indoc! {"
29780 - item
29781 - sub item
29782 - ˇ
29783 "});
29784 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29785 cx.wait_for_autoindent_applied().await;
29786 cx.assert_editor_state(indoc! {"
29787 - item
29788 - sub item
29789 - ˇ
29790 "});
29791 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29792 cx.wait_for_autoindent_applied().await;
29793
29794 // Case 7: Adding newline with cursor right after marker, removes marker
29795 cx.assert_editor_state(indoc! {"
29796 - item
29797 - sub item
29798 - ˇ
29799 "});
29800 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29801 cx.wait_for_autoindent_applied().await;
29802 cx.assert_editor_state(indoc! {"
29803 - item
29804 - sub item
29805 ˇ
29806 "});
29807
29808 // Case 8: Cursor before or inside prefix does not add marker
29809 cx.set_state(indoc! {"
29810 ˇ- item
29811 "});
29812 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29813 cx.wait_for_autoindent_applied().await;
29814 cx.assert_editor_state(indoc! {"
29815
29816 ˇ- item
29817 "});
29818
29819 cx.set_state(indoc! {"
29820 -ˇ item
29821 "});
29822 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29823 cx.wait_for_autoindent_applied().await;
29824 cx.assert_editor_state(indoc! {"
29825 -
29826 ˇitem
29827 "});
29828}
29829
29830#[gpui::test]
29831async fn test_newline_ordered_list_continuation(cx: &mut TestAppContext) {
29832 init_test(cx, |settings| {
29833 settings.defaults.tab_size = Some(2.try_into().unwrap());
29834 });
29835
29836 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29837 let mut cx = EditorTestContext::new(cx).await;
29838 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29839
29840 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
29841 cx.set_state(indoc! {"
29842 1. first itemˇ
29843 "});
29844 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29845 cx.wait_for_autoindent_applied().await;
29846 cx.assert_editor_state(indoc! {"
29847 1. first item
29848 2. ˇ
29849 "});
29850
29851 // Case 2: Works with larger numbers
29852 cx.set_state(indoc! {"
29853 10. tenth itemˇ
29854 "});
29855 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29856 cx.wait_for_autoindent_applied().await;
29857 cx.assert_editor_state(indoc! {"
29858 10. tenth item
29859 11. ˇ
29860 "});
29861
29862 // Case 3: Cursor position doesn't matter - content after marker is what counts
29863 cx.set_state(indoc! {"
29864 1. itˇem
29865 "});
29866 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29867 cx.wait_for_autoindent_applied().await;
29868 cx.assert_editor_state(indoc! {"
29869 1. it
29870 2. ˇem
29871 "});
29872
29873 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
29874 cx.set_state(indoc! {"
29875 1. ˇ
29876 "});
29877 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29878 cx.wait_for_autoindent_applied().await;
29879 cx.assert_editor_state(
29880 indoc! {"
29881 1. $
29882 ˇ
29883 "}
29884 .replace("$", " ")
29885 .as_str(),
29886 );
29887
29888 // Case 5: Adding newline with content adds marker preserving indentation
29889 cx.set_state(indoc! {"
29890 1. item
29891 2. indentedˇ
29892 "});
29893 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29894 cx.wait_for_autoindent_applied().await;
29895 cx.assert_editor_state(indoc! {"
29896 1. item
29897 2. indented
29898 3. ˇ
29899 "});
29900
29901 // Case 6: Adding newline with cursor right after marker, unindents
29902 cx.set_state(indoc! {"
29903 1. item
29904 2. sub item
29905 3. ˇ
29906 "});
29907 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29908 cx.wait_for_autoindent_applied().await;
29909 cx.assert_editor_state(indoc! {"
29910 1. item
29911 2. sub item
29912 1. ˇ
29913 "});
29914 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29915 cx.wait_for_autoindent_applied().await;
29916
29917 // Case 7: Adding newline with cursor right after marker, removes marker
29918 cx.assert_editor_state(indoc! {"
29919 1. item
29920 2. sub item
29921 1. ˇ
29922 "});
29923 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29924 cx.wait_for_autoindent_applied().await;
29925 cx.assert_editor_state(indoc! {"
29926 1. item
29927 2. sub item
29928 ˇ
29929 "});
29930
29931 // Case 8: Cursor before or inside prefix does not add marker
29932 cx.set_state(indoc! {"
29933 ˇ1. item
29934 "});
29935 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29936 cx.wait_for_autoindent_applied().await;
29937 cx.assert_editor_state(indoc! {"
29938
29939 ˇ1. item
29940 "});
29941
29942 cx.set_state(indoc! {"
29943 1ˇ. item
29944 "});
29945 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29946 cx.wait_for_autoindent_applied().await;
29947 cx.assert_editor_state(indoc! {"
29948 1
29949 ˇ. item
29950 "});
29951}
29952
29953#[gpui::test]
29954async fn test_newline_should_not_autoindent_ordered_list(cx: &mut TestAppContext) {
29955 init_test(cx, |settings| {
29956 settings.defaults.tab_size = Some(2.try_into().unwrap());
29957 });
29958
29959 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29960 let mut cx = EditorTestContext::new(cx).await;
29961 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29962
29963 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
29964 cx.set_state(indoc! {"
29965 1. first item
29966 1. sub first item
29967 2. sub second item
29968 3. ˇ
29969 "});
29970 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29971 cx.wait_for_autoindent_applied().await;
29972 cx.assert_editor_state(indoc! {"
29973 1. first item
29974 1. sub first item
29975 2. sub second item
29976 1. ˇ
29977 "});
29978}
29979
29980#[gpui::test]
29981async fn test_tab_list_indent(cx: &mut TestAppContext) {
29982 init_test(cx, |settings| {
29983 settings.defaults.tab_size = Some(2.try_into().unwrap());
29984 });
29985
29986 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29987 let mut cx = EditorTestContext::new(cx).await;
29988 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29989
29990 // Case 1: Unordered list - cursor after prefix, adds indent before prefix
29991 cx.set_state(indoc! {"
29992 - ˇitem
29993 "});
29994 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
29995 cx.wait_for_autoindent_applied().await;
29996 let expected = indoc! {"
29997 $$- ˇitem
29998 "};
29999 cx.assert_editor_state(expected.replace("$", " ").as_str());
30000
30001 // Case 2: Task list - cursor after prefix
30002 cx.set_state(indoc! {"
30003 - [ ] ˇtask
30004 "});
30005 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30006 cx.wait_for_autoindent_applied().await;
30007 let expected = indoc! {"
30008 $$- [ ] ˇtask
30009 "};
30010 cx.assert_editor_state(expected.replace("$", " ").as_str());
30011
30012 // Case 3: Ordered list - cursor after prefix
30013 cx.set_state(indoc! {"
30014 1. ˇfirst
30015 "});
30016 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30017 cx.wait_for_autoindent_applied().await;
30018 let expected = indoc! {"
30019 $$1. ˇfirst
30020 "};
30021 cx.assert_editor_state(expected.replace("$", " ").as_str());
30022
30023 // Case 4: With existing indentation - adds more indent
30024 let initial = indoc! {"
30025 $$- ˇitem
30026 "};
30027 cx.set_state(initial.replace("$", " ").as_str());
30028 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30029 cx.wait_for_autoindent_applied().await;
30030 let expected = indoc! {"
30031 $$$$- ˇitem
30032 "};
30033 cx.assert_editor_state(expected.replace("$", " ").as_str());
30034
30035 // Case 5: Empty list item
30036 cx.set_state(indoc! {"
30037 - ˇ
30038 "});
30039 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30040 cx.wait_for_autoindent_applied().await;
30041 let expected = indoc! {"
30042 $$- ˇ
30043 "};
30044 cx.assert_editor_state(expected.replace("$", " ").as_str());
30045
30046 // Case 6: Cursor at end of line with content
30047 cx.set_state(indoc! {"
30048 - itemˇ
30049 "});
30050 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30051 cx.wait_for_autoindent_applied().await;
30052 let expected = indoc! {"
30053 $$- itemˇ
30054 "};
30055 cx.assert_editor_state(expected.replace("$", " ").as_str());
30056
30057 // Case 7: Cursor at start of list item, indents it
30058 cx.set_state(indoc! {"
30059 - item
30060 ˇ - sub item
30061 "});
30062 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30063 cx.wait_for_autoindent_applied().await;
30064 let expected = indoc! {"
30065 - item
30066 ˇ - sub item
30067 "};
30068 cx.assert_editor_state(expected);
30069
30070 // Case 8: Cursor at start of list item, moves the cursor when "indent_list_on_tab" is false
30071 cx.update_editor(|_, _, cx| {
30072 SettingsStore::update_global(cx, |store, cx| {
30073 store.update_user_settings(cx, |settings| {
30074 settings.project.all_languages.defaults.indent_list_on_tab = Some(false);
30075 });
30076 });
30077 });
30078 cx.set_state(indoc! {"
30079 - item
30080 ˇ - sub item
30081 "});
30082 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30083 cx.wait_for_autoindent_applied().await;
30084 let expected = indoc! {"
30085 - item
30086 ˇ- sub item
30087 "};
30088 cx.assert_editor_state(expected);
30089}
30090
30091#[gpui::test]
30092async fn test_local_worktree_trust(cx: &mut TestAppContext) {
30093 init_test(cx, |_| {});
30094 cx.update(|cx| project::trusted_worktrees::init(HashMap::default(), None, None, cx));
30095
30096 cx.update(|cx| {
30097 SettingsStore::update_global(cx, |store, cx| {
30098 store.update_user_settings(cx, |settings| {
30099 settings.project.all_languages.defaults.inlay_hints =
30100 Some(InlayHintSettingsContent {
30101 enabled: Some(true),
30102 ..InlayHintSettingsContent::default()
30103 });
30104 });
30105 });
30106 });
30107
30108 let fs = FakeFs::new(cx.executor());
30109 fs.insert_tree(
30110 path!("/project"),
30111 json!({
30112 ".zed": {
30113 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
30114 },
30115 "main.rs": "fn main() {}"
30116 }),
30117 )
30118 .await;
30119
30120 let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
30121 let server_name = "override-rust-analyzer";
30122 let project = Project::test_with_worktree_trust(fs, [path!("/project").as_ref()], cx).await;
30123
30124 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
30125 language_registry.add(rust_lang());
30126
30127 let capabilities = lsp::ServerCapabilities {
30128 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
30129 ..lsp::ServerCapabilities::default()
30130 };
30131 let mut fake_language_servers = language_registry.register_fake_lsp(
30132 "Rust",
30133 FakeLspAdapter {
30134 name: server_name,
30135 capabilities,
30136 initializer: Some(Box::new({
30137 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
30138 move |fake_server| {
30139 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
30140 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
30141 move |_params, _| {
30142 lsp_inlay_hint_request_count.fetch_add(1, atomic::Ordering::Release);
30143 async move {
30144 Ok(Some(vec![lsp::InlayHint {
30145 position: lsp::Position::new(0, 0),
30146 label: lsp::InlayHintLabel::String("hint".to_string()),
30147 kind: None,
30148 text_edits: None,
30149 tooltip: None,
30150 padding_left: None,
30151 padding_right: None,
30152 data: None,
30153 }]))
30154 }
30155 },
30156 );
30157 }
30158 })),
30159 ..FakeLspAdapter::default()
30160 },
30161 );
30162
30163 cx.run_until_parked();
30164
30165 let worktree_id = project.read_with(cx, |project, cx| {
30166 project
30167 .worktrees(cx)
30168 .next()
30169 .map(|wt| wt.read(cx).id())
30170 .expect("should have a worktree")
30171 });
30172
30173 let trusted_worktrees =
30174 cx.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
30175
30176 let can_trust = trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
30177 assert!(!can_trust, "worktree should be restricted initially");
30178
30179 let buffer_before_approval = project
30180 .update(cx, |project, cx| {
30181 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
30182 })
30183 .await
30184 .unwrap();
30185
30186 let (editor, cx) = cx.add_window_view(|window, cx| {
30187 Editor::new(
30188 EditorMode::full(),
30189 cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
30190 Some(project.clone()),
30191 window,
30192 cx,
30193 )
30194 });
30195 cx.run_until_parked();
30196 let fake_language_server = fake_language_servers.next();
30197
30198 cx.read(|cx| {
30199 let file = buffer_before_approval.read(cx).file();
30200 assert_eq!(
30201 language::language_settings::language_settings(Some("Rust".into()), file, cx)
30202 .language_servers,
30203 ["...".to_string()],
30204 "local .zed/settings.json must not apply before trust approval"
30205 )
30206 });
30207
30208 editor.update_in(cx, |editor, window, cx| {
30209 editor.handle_input("1", window, cx);
30210 });
30211 cx.run_until_parked();
30212 cx.executor()
30213 .advance_clock(std::time::Duration::from_secs(1));
30214 assert_eq!(
30215 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire),
30216 0,
30217 "inlay hints must not be queried before trust approval"
30218 );
30219
30220 trusted_worktrees.update(cx, |store, cx| {
30221 store.trust(
30222 std::collections::HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
30223 None,
30224 cx,
30225 );
30226 });
30227 cx.run_until_parked();
30228
30229 cx.read(|cx| {
30230 let file = buffer_before_approval.read(cx).file();
30231 assert_eq!(
30232 language::language_settings::language_settings(Some("Rust".into()), file, cx)
30233 .language_servers,
30234 ["override-rust-analyzer".to_string()],
30235 "local .zed/settings.json should apply after trust approval"
30236 )
30237 });
30238 let _fake_language_server = fake_language_server.await.unwrap();
30239 editor.update_in(cx, |editor, window, cx| {
30240 editor.handle_input("1", window, cx);
30241 });
30242 cx.run_until_parked();
30243 cx.executor()
30244 .advance_clock(std::time::Duration::from_secs(1));
30245 assert!(
30246 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire) > 0,
30247 "inlay hints should be queried after trust approval"
30248 );
30249
30250 let can_trust_after =
30251 trusted_worktrees.update(cx, |store, cx| store.can_trust(worktree_id, cx));
30252 assert!(can_trust_after, "worktree should be trusted after trust()");
30253}
30254
30255#[gpui::test]
30256fn test_editor_rendering_when_positioned_above_viewport(cx: &mut TestAppContext) {
30257 // This test reproduces a bug where drawing an editor at a position above the viewport
30258 // (simulating what happens when an AutoHeight editor inside a List is scrolled past)
30259 // causes an infinite loop in blocks_in_range.
30260 //
30261 // The issue: when the editor's bounds.origin.y is very negative (above the viewport),
30262 // the content mask intersection produces visible_bounds with origin at the viewport top.
30263 // This makes clipped_top_in_lines very large, causing start_row to exceed max_row.
30264 // When blocks_in_range is called with start_row > max_row, the cursor seeks to the end
30265 // but the while loop after seek never terminates because cursor.next() is a no-op at end.
30266 init_test(cx, |_| {});
30267
30268 let window = cx.add_window(|_, _| gpui::Empty);
30269 let mut cx = VisualTestContext::from_window(*window, cx);
30270
30271 let buffer = cx.update(|_, cx| MultiBuffer::build_simple("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n", cx));
30272 let editor = cx.new_window_entity(|window, cx| build_editor(buffer, window, cx));
30273
30274 // Simulate a small viewport (500x500 pixels at origin 0,0)
30275 cx.simulate_resize(gpui::size(px(500.), px(500.)));
30276
30277 // Draw the editor at a very negative Y position, simulating an editor that's been
30278 // scrolled way above the visible viewport (like in a List that has scrolled past it).
30279 // The editor is 3000px tall but positioned at y=-10000, so it's entirely above the viewport.
30280 // This should NOT hang - it should just render nothing.
30281 cx.draw(
30282 gpui::point(px(0.), px(-10000.)),
30283 gpui::size(px(500.), px(3000.)),
30284 |_, _| editor.clone(),
30285 );
30286
30287 // If we get here without hanging, the test passes
30288}