1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use collections::HashMap;
17use futures::{StreamExt, channel::oneshot};
18use gpui::{
19 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
20 VisualTestContext, WindowBounds, WindowOptions, div,
21};
22use indoc::indoc;
23use language::{
24 BracketPairConfig,
25 Capability::ReadWrite,
26 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
27 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
28 language_settings::{
29 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
30 },
31 tree_sitter_python,
32};
33use language_settings::Formatter;
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::LspSettings,
42};
43use serde_json::{self, json};
44use settings::{
45 AllLanguageSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring,
46 ProjectSettingsContent,
47};
48use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
49use std::{
50 iter,
51 sync::atomic::{self, AtomicUsize},
52};
53use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
54use text::ToPoint as _;
55use unindent::Unindent;
56use util::{
57 assert_set_eq, path,
58 rel_path::rel_path,
59 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
60 uri,
61};
62use workspace::{
63 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
64 OpenOptions, ViewId,
65 invalid_buffer_view::InvalidBufferView,
66 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
67 register_project_item,
68};
69
70#[gpui::test]
71fn test_edit_events(cx: &mut TestAppContext) {
72 init_test(cx, |_| {});
73
74 let buffer = cx.new(|cx| {
75 let mut buffer = language::Buffer::local("123456", cx);
76 buffer.set_group_interval(Duration::from_secs(1));
77 buffer
78 });
79
80 let events = Rc::new(RefCell::new(Vec::new()));
81 let editor1 = cx.add_window({
82 let events = events.clone();
83 |window, cx| {
84 let entity = cx.entity();
85 cx.subscribe_in(
86 &entity,
87 window,
88 move |_, _, event: &EditorEvent, _, _| match event {
89 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
90 EditorEvent::BufferEdited => {
91 events.borrow_mut().push(("editor1", "buffer edited"))
92 }
93 _ => {}
94 },
95 )
96 .detach();
97 Editor::for_buffer(buffer.clone(), None, window, cx)
98 }
99 });
100
101 let editor2 = cx.add_window({
102 let events = events.clone();
103 |window, cx| {
104 cx.subscribe_in(
105 &cx.entity(),
106 window,
107 move |_, _, event: &EditorEvent, _, _| match event {
108 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
109 EditorEvent::BufferEdited => {
110 events.borrow_mut().push(("editor2", "buffer edited"))
111 }
112 _ => {}
113 },
114 )
115 .detach();
116 Editor::for_buffer(buffer.clone(), None, window, cx)
117 }
118 });
119
120 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
121
122 // Mutating editor 1 will emit an `Edited` event only for that editor.
123 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
124 assert_eq!(
125 mem::take(&mut *events.borrow_mut()),
126 [
127 ("editor1", "edited"),
128 ("editor1", "buffer edited"),
129 ("editor2", "buffer edited"),
130 ]
131 );
132
133 // Mutating editor 2 will emit an `Edited` event only for that editor.
134 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
135 assert_eq!(
136 mem::take(&mut *events.borrow_mut()),
137 [
138 ("editor2", "edited"),
139 ("editor1", "buffer edited"),
140 ("editor2", "buffer edited"),
141 ]
142 );
143
144 // Undoing on editor 1 will emit an `Edited` event only for that editor.
145 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
146 assert_eq!(
147 mem::take(&mut *events.borrow_mut()),
148 [
149 ("editor1", "edited"),
150 ("editor1", "buffer edited"),
151 ("editor2", "buffer edited"),
152 ]
153 );
154
155 // Redoing on editor 1 will emit an `Edited` event only for that editor.
156 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
157 assert_eq!(
158 mem::take(&mut *events.borrow_mut()),
159 [
160 ("editor1", "edited"),
161 ("editor1", "buffer edited"),
162 ("editor2", "buffer edited"),
163 ]
164 );
165
166 // Undoing on editor 2 will emit an `Edited` event only for that editor.
167 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
168 assert_eq!(
169 mem::take(&mut *events.borrow_mut()),
170 [
171 ("editor2", "edited"),
172 ("editor1", "buffer edited"),
173 ("editor2", "buffer edited"),
174 ]
175 );
176
177 // Redoing on editor 2 will emit an `Edited` event only for that editor.
178 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
179 assert_eq!(
180 mem::take(&mut *events.borrow_mut()),
181 [
182 ("editor2", "edited"),
183 ("editor1", "buffer edited"),
184 ("editor2", "buffer edited"),
185 ]
186 );
187
188 // No event is emitted when the mutation is a no-op.
189 _ = editor2.update(cx, |editor, window, cx| {
190 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
191 s.select_ranges([0..0])
192 });
193
194 editor.backspace(&Backspace, window, cx);
195 });
196 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
197}
198
199#[gpui::test]
200fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
201 init_test(cx, |_| {});
202
203 let mut now = Instant::now();
204 let group_interval = Duration::from_millis(1);
205 let buffer = cx.new(|cx| {
206 let mut buf = language::Buffer::local("123456", cx);
207 buf.set_group_interval(group_interval);
208 buf
209 });
210 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
211 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
212
213 _ = editor.update(cx, |editor, window, cx| {
214 editor.start_transaction_at(now, window, cx);
215 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
216 s.select_ranges([2..4])
217 });
218
219 editor.insert("cd", window, cx);
220 editor.end_transaction_at(now, cx);
221 assert_eq!(editor.text(cx), "12cd56");
222 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
223
224 editor.start_transaction_at(now, window, cx);
225 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
226 s.select_ranges([4..5])
227 });
228 editor.insert("e", window, cx);
229 editor.end_transaction_at(now, cx);
230 assert_eq!(editor.text(cx), "12cde6");
231 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
232
233 now += group_interval + Duration::from_millis(1);
234 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
235 s.select_ranges([2..2])
236 });
237
238 // Simulate an edit in another editor
239 buffer.update(cx, |buffer, cx| {
240 buffer.start_transaction_at(now, cx);
241 buffer.edit([(0..1, "a")], None, cx);
242 buffer.edit([(1..1, "b")], None, cx);
243 buffer.end_transaction_at(now, cx);
244 });
245
246 assert_eq!(editor.text(cx), "ab2cde6");
247 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
248
249 // Last transaction happened past the group interval in a different editor.
250 // Undo it individually and don't restore selections.
251 editor.undo(&Undo, window, cx);
252 assert_eq!(editor.text(cx), "12cde6");
253 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
254
255 // First two transactions happened within the group interval in this editor.
256 // Undo them together and restore selections.
257 editor.undo(&Undo, window, cx);
258 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
259 assert_eq!(editor.text(cx), "123456");
260 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
261
262 // Redo the first two transactions together.
263 editor.redo(&Redo, window, cx);
264 assert_eq!(editor.text(cx), "12cde6");
265 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
266
267 // Redo the last transaction on its own.
268 editor.redo(&Redo, window, cx);
269 assert_eq!(editor.text(cx), "ab2cde6");
270 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
271
272 // Test empty transactions.
273 editor.start_transaction_at(now, window, cx);
274 editor.end_transaction_at(now, cx);
275 editor.undo(&Undo, window, cx);
276 assert_eq!(editor.text(cx), "12cde6");
277 });
278}
279
280#[gpui::test]
281fn test_ime_composition(cx: &mut TestAppContext) {
282 init_test(cx, |_| {});
283
284 let buffer = cx.new(|cx| {
285 let mut buffer = language::Buffer::local("abcde", cx);
286 // Ensure automatic grouping doesn't occur.
287 buffer.set_group_interval(Duration::ZERO);
288 buffer
289 });
290
291 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
292 cx.add_window(|window, cx| {
293 let mut editor = build_editor(buffer.clone(), window, cx);
294
295 // Start a new IME composition.
296 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
297 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
298 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
299 assert_eq!(editor.text(cx), "äbcde");
300 assert_eq!(
301 editor.marked_text_ranges(cx),
302 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
303 );
304
305 // Finalize IME composition.
306 editor.replace_text_in_range(None, "ā", window, cx);
307 assert_eq!(editor.text(cx), "ābcde");
308 assert_eq!(editor.marked_text_ranges(cx), None);
309
310 // IME composition edits are grouped and are undone/redone at once.
311 editor.undo(&Default::default(), window, cx);
312 assert_eq!(editor.text(cx), "abcde");
313 assert_eq!(editor.marked_text_ranges(cx), None);
314 editor.redo(&Default::default(), window, cx);
315 assert_eq!(editor.text(cx), "ābcde");
316 assert_eq!(editor.marked_text_ranges(cx), None);
317
318 // Start a new IME composition.
319 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
320 assert_eq!(
321 editor.marked_text_ranges(cx),
322 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
323 );
324
325 // Undoing during an IME composition cancels it.
326 editor.undo(&Default::default(), window, cx);
327 assert_eq!(editor.text(cx), "ābcde");
328 assert_eq!(editor.marked_text_ranges(cx), None);
329
330 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
331 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
332 assert_eq!(editor.text(cx), "ābcdè");
333 assert_eq!(
334 editor.marked_text_ranges(cx),
335 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
336 );
337
338 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
339 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
340 assert_eq!(editor.text(cx), "ābcdę");
341 assert_eq!(editor.marked_text_ranges(cx), None);
342
343 // Start a new IME composition with multiple cursors.
344 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
345 s.select_ranges([
346 OffsetUtf16(1)..OffsetUtf16(1),
347 OffsetUtf16(3)..OffsetUtf16(3),
348 OffsetUtf16(5)..OffsetUtf16(5),
349 ])
350 });
351 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
352 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
353 assert_eq!(
354 editor.marked_text_ranges(cx),
355 Some(vec![
356 OffsetUtf16(0)..OffsetUtf16(3),
357 OffsetUtf16(4)..OffsetUtf16(7),
358 OffsetUtf16(8)..OffsetUtf16(11)
359 ])
360 );
361
362 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
363 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
364 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
365 assert_eq!(
366 editor.marked_text_ranges(cx),
367 Some(vec![
368 OffsetUtf16(1)..OffsetUtf16(2),
369 OffsetUtf16(5)..OffsetUtf16(6),
370 OffsetUtf16(9)..OffsetUtf16(10)
371 ])
372 );
373
374 // Finalize IME composition with multiple cursors.
375 editor.replace_text_in_range(Some(9..10), "2", window, cx);
376 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
377 assert_eq!(editor.marked_text_ranges(cx), None);
378
379 editor
380 });
381}
382
383#[gpui::test]
384fn test_selection_with_mouse(cx: &mut TestAppContext) {
385 init_test(cx, |_| {});
386
387 let editor = cx.add_window(|window, cx| {
388 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
389 build_editor(buffer, window, cx)
390 });
391
392 _ = editor.update(cx, |editor, window, cx| {
393 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
394 });
395 assert_eq!(
396 editor
397 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
398 .unwrap(),
399 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
400 );
401
402 _ = editor.update(cx, |editor, window, cx| {
403 editor.update_selection(
404 DisplayPoint::new(DisplayRow(3), 3),
405 0,
406 gpui::Point::<f32>::default(),
407 window,
408 cx,
409 );
410 });
411
412 assert_eq!(
413 editor
414 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
415 .unwrap(),
416 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
417 );
418
419 _ = editor.update(cx, |editor, window, cx| {
420 editor.update_selection(
421 DisplayPoint::new(DisplayRow(1), 1),
422 0,
423 gpui::Point::<f32>::default(),
424 window,
425 cx,
426 );
427 });
428
429 assert_eq!(
430 editor
431 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
432 .unwrap(),
433 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
434 );
435
436 _ = editor.update(cx, |editor, window, cx| {
437 editor.end_selection(window, cx);
438 editor.update_selection(
439 DisplayPoint::new(DisplayRow(3), 3),
440 0,
441 gpui::Point::<f32>::default(),
442 window,
443 cx,
444 );
445 });
446
447 assert_eq!(
448 editor
449 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
450 .unwrap(),
451 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
452 );
453
454 _ = editor.update(cx, |editor, window, cx| {
455 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
456 editor.update_selection(
457 DisplayPoint::new(DisplayRow(0), 0),
458 0,
459 gpui::Point::<f32>::default(),
460 window,
461 cx,
462 );
463 });
464
465 assert_eq!(
466 editor
467 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
468 .unwrap(),
469 [
470 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
471 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
472 ]
473 );
474
475 _ = editor.update(cx, |editor, window, cx| {
476 editor.end_selection(window, cx);
477 });
478
479 assert_eq!(
480 editor
481 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
482 .unwrap(),
483 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
484 );
485}
486
487#[gpui::test]
488fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
489 init_test(cx, |_| {});
490
491 let editor = cx.add_window(|window, cx| {
492 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
493 build_editor(buffer, window, cx)
494 });
495
496 _ = editor.update(cx, |editor, window, cx| {
497 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
498 });
499
500 _ = editor.update(cx, |editor, window, cx| {
501 editor.end_selection(window, cx);
502 });
503
504 _ = editor.update(cx, |editor, window, cx| {
505 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
506 });
507
508 _ = editor.update(cx, |editor, window, cx| {
509 editor.end_selection(window, cx);
510 });
511
512 assert_eq!(
513 editor
514 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
515 .unwrap(),
516 [
517 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
518 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
519 ]
520 );
521
522 _ = editor.update(cx, |editor, window, cx| {
523 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
524 });
525
526 _ = editor.update(cx, |editor, window, cx| {
527 editor.end_selection(window, cx);
528 });
529
530 assert_eq!(
531 editor
532 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
533 .unwrap(),
534 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
535 );
536}
537
538#[gpui::test]
539fn test_canceling_pending_selection(cx: &mut TestAppContext) {
540 init_test(cx, |_| {});
541
542 let editor = cx.add_window(|window, cx| {
543 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
544 build_editor(buffer, window, cx)
545 });
546
547 _ = editor.update(cx, |editor, window, cx| {
548 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
549 assert_eq!(
550 editor.selections.display_ranges(cx),
551 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
552 );
553 });
554
555 _ = editor.update(cx, |editor, window, cx| {
556 editor.update_selection(
557 DisplayPoint::new(DisplayRow(3), 3),
558 0,
559 gpui::Point::<f32>::default(),
560 window,
561 cx,
562 );
563 assert_eq!(
564 editor.selections.display_ranges(cx),
565 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
566 );
567 });
568
569 _ = editor.update(cx, |editor, window, cx| {
570 editor.cancel(&Cancel, window, cx);
571 editor.update_selection(
572 DisplayPoint::new(DisplayRow(1), 1),
573 0,
574 gpui::Point::<f32>::default(),
575 window,
576 cx,
577 );
578 assert_eq!(
579 editor.selections.display_ranges(cx),
580 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
581 );
582 });
583}
584
585#[gpui::test]
586fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
587 init_test(cx, |_| {});
588
589 let editor = cx.add_window(|window, cx| {
590 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
591 build_editor(buffer, window, cx)
592 });
593
594 _ = editor.update(cx, |editor, window, cx| {
595 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
596 assert_eq!(
597 editor.selections.display_ranges(cx),
598 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
599 );
600
601 editor.move_down(&Default::default(), window, cx);
602 assert_eq!(
603 editor.selections.display_ranges(cx),
604 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
605 );
606
607 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
608 assert_eq!(
609 editor.selections.display_ranges(cx),
610 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
611 );
612
613 editor.move_up(&Default::default(), window, cx);
614 assert_eq!(
615 editor.selections.display_ranges(cx),
616 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
617 );
618 });
619}
620
621#[gpui::test]
622fn test_extending_selection(cx: &mut TestAppContext) {
623 init_test(cx, |_| {});
624
625 let editor = cx.add_window(|window, cx| {
626 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
627 build_editor(buffer, window, cx)
628 });
629
630 _ = editor.update(cx, |editor, window, cx| {
631 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
632 editor.end_selection(window, cx);
633 assert_eq!(
634 editor.selections.display_ranges(cx),
635 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
636 );
637
638 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
639 editor.end_selection(window, cx);
640 assert_eq!(
641 editor.selections.display_ranges(cx),
642 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
643 );
644
645 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
646 editor.end_selection(window, cx);
647 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
648 assert_eq!(
649 editor.selections.display_ranges(cx),
650 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
651 );
652
653 editor.update_selection(
654 DisplayPoint::new(DisplayRow(0), 1),
655 0,
656 gpui::Point::<f32>::default(),
657 window,
658 cx,
659 );
660 editor.end_selection(window, cx);
661 assert_eq!(
662 editor.selections.display_ranges(cx),
663 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
664 );
665
666 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
667 editor.end_selection(window, cx);
668 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
669 editor.end_selection(window, cx);
670 assert_eq!(
671 editor.selections.display_ranges(cx),
672 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
673 );
674
675 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
676 assert_eq!(
677 editor.selections.display_ranges(cx),
678 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
679 );
680
681 editor.update_selection(
682 DisplayPoint::new(DisplayRow(0), 6),
683 0,
684 gpui::Point::<f32>::default(),
685 window,
686 cx,
687 );
688 assert_eq!(
689 editor.selections.display_ranges(cx),
690 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
691 );
692
693 editor.update_selection(
694 DisplayPoint::new(DisplayRow(0), 1),
695 0,
696 gpui::Point::<f32>::default(),
697 window,
698 cx,
699 );
700 editor.end_selection(window, cx);
701 assert_eq!(
702 editor.selections.display_ranges(cx),
703 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
704 );
705 });
706}
707
708#[gpui::test]
709fn test_clone(cx: &mut TestAppContext) {
710 init_test(cx, |_| {});
711
712 let (text, selection_ranges) = marked_text_ranges(
713 indoc! {"
714 one
715 two
716 threeˇ
717 four
718 fiveˇ
719 "},
720 true,
721 );
722
723 let editor = cx.add_window(|window, cx| {
724 let buffer = MultiBuffer::build_simple(&text, cx);
725 build_editor(buffer, window, cx)
726 });
727
728 _ = editor.update(cx, |editor, window, cx| {
729 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
730 s.select_ranges(selection_ranges.clone())
731 });
732 editor.fold_creases(
733 vec![
734 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
735 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
736 ],
737 true,
738 window,
739 cx,
740 );
741 });
742
743 let cloned_editor = editor
744 .update(cx, |editor, _, cx| {
745 cx.open_window(Default::default(), |window, cx| {
746 cx.new(|cx| editor.clone(window, cx))
747 })
748 })
749 .unwrap()
750 .unwrap();
751
752 let snapshot = editor
753 .update(cx, |e, window, cx| e.snapshot(window, cx))
754 .unwrap();
755 let cloned_snapshot = cloned_editor
756 .update(cx, |e, window, cx| e.snapshot(window, cx))
757 .unwrap();
758
759 assert_eq!(
760 cloned_editor
761 .update(cx, |e, _, cx| e.display_text(cx))
762 .unwrap(),
763 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
764 );
765 assert_eq!(
766 cloned_snapshot
767 .folds_in_range(0..text.len())
768 .collect::<Vec<_>>(),
769 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
770 );
771 assert_set_eq!(
772 cloned_editor
773 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
774 .unwrap(),
775 editor
776 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
777 .unwrap()
778 );
779 assert_set_eq!(
780 cloned_editor
781 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
782 .unwrap(),
783 editor
784 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
785 .unwrap()
786 );
787}
788
789#[gpui::test]
790async fn test_navigation_history(cx: &mut TestAppContext) {
791 init_test(cx, |_| {});
792
793 use workspace::item::Item;
794
795 let fs = FakeFs::new(cx.executor());
796 let project = Project::test(fs, [], cx).await;
797 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
798 let pane = workspace
799 .update(cx, |workspace, _, _| workspace.active_pane().clone())
800 .unwrap();
801
802 _ = workspace.update(cx, |_v, window, cx| {
803 cx.new(|cx| {
804 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
805 let mut editor = build_editor(buffer, window, cx);
806 let handle = cx.entity();
807 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
808
809 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
810 editor.nav_history.as_mut().unwrap().pop_backward(cx)
811 }
812
813 // Move the cursor a small distance.
814 // Nothing is added to the navigation history.
815 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
816 s.select_display_ranges([
817 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
818 ])
819 });
820 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
821 s.select_display_ranges([
822 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
823 ])
824 });
825 assert!(pop_history(&mut editor, cx).is_none());
826
827 // Move the cursor a large distance.
828 // The history can jump back to the previous position.
829 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
830 s.select_display_ranges([
831 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
832 ])
833 });
834 let nav_entry = pop_history(&mut editor, cx).unwrap();
835 editor.navigate(nav_entry.data.unwrap(), window, cx);
836 assert_eq!(nav_entry.item.id(), cx.entity_id());
837 assert_eq!(
838 editor.selections.display_ranges(cx),
839 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
840 );
841 assert!(pop_history(&mut editor, cx).is_none());
842
843 // Move the cursor a small distance via the mouse.
844 // Nothing is added to the navigation history.
845 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
846 editor.end_selection(window, cx);
847 assert_eq!(
848 editor.selections.display_ranges(cx),
849 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
850 );
851 assert!(pop_history(&mut editor, cx).is_none());
852
853 // Move the cursor a large distance via the mouse.
854 // The history can jump back to the previous position.
855 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
856 editor.end_selection(window, cx);
857 assert_eq!(
858 editor.selections.display_ranges(cx),
859 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
860 );
861 let nav_entry = pop_history(&mut editor, cx).unwrap();
862 editor.navigate(nav_entry.data.unwrap(), window, cx);
863 assert_eq!(nav_entry.item.id(), cx.entity_id());
864 assert_eq!(
865 editor.selections.display_ranges(cx),
866 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
867 );
868 assert!(pop_history(&mut editor, cx).is_none());
869
870 // Set scroll position to check later
871 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
872 let original_scroll_position = editor.scroll_manager.anchor();
873
874 // Jump to the end of the document and adjust scroll
875 editor.move_to_end(&MoveToEnd, window, cx);
876 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
877 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
878
879 let nav_entry = pop_history(&mut editor, cx).unwrap();
880 editor.navigate(nav_entry.data.unwrap(), window, cx);
881 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
882
883 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
884 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
885 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
886 let invalid_point = Point::new(9999, 0);
887 editor.navigate(
888 Box::new(NavigationData {
889 cursor_anchor: invalid_anchor,
890 cursor_position: invalid_point,
891 scroll_anchor: ScrollAnchor {
892 anchor: invalid_anchor,
893 offset: Default::default(),
894 },
895 scroll_top_row: invalid_point.row,
896 }),
897 window,
898 cx,
899 );
900 assert_eq!(
901 editor.selections.display_ranges(cx),
902 &[editor.max_point(cx)..editor.max_point(cx)]
903 );
904 assert_eq!(
905 editor.scroll_position(cx),
906 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
907 );
908
909 editor
910 })
911 });
912}
913
914#[gpui::test]
915fn test_cancel(cx: &mut TestAppContext) {
916 init_test(cx, |_| {});
917
918 let editor = cx.add_window(|window, cx| {
919 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
920 build_editor(buffer, window, cx)
921 });
922
923 _ = editor.update(cx, |editor, window, cx| {
924 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
925 editor.update_selection(
926 DisplayPoint::new(DisplayRow(1), 1),
927 0,
928 gpui::Point::<f32>::default(),
929 window,
930 cx,
931 );
932 editor.end_selection(window, cx);
933
934 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
935 editor.update_selection(
936 DisplayPoint::new(DisplayRow(0), 3),
937 0,
938 gpui::Point::<f32>::default(),
939 window,
940 cx,
941 );
942 editor.end_selection(window, cx);
943 assert_eq!(
944 editor.selections.display_ranges(cx),
945 [
946 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
947 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
948 ]
949 );
950 });
951
952 _ = editor.update(cx, |editor, window, cx| {
953 editor.cancel(&Cancel, window, cx);
954 assert_eq!(
955 editor.selections.display_ranges(cx),
956 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
957 );
958 });
959
960 _ = editor.update(cx, |editor, window, cx| {
961 editor.cancel(&Cancel, window, cx);
962 assert_eq!(
963 editor.selections.display_ranges(cx),
964 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
965 );
966 });
967}
968
969#[gpui::test]
970fn test_fold_action(cx: &mut TestAppContext) {
971 init_test(cx, |_| {});
972
973 let editor = cx.add_window(|window, cx| {
974 let buffer = MultiBuffer::build_simple(
975 &"
976 impl Foo {
977 // Hello!
978
979 fn a() {
980 1
981 }
982
983 fn b() {
984 2
985 }
986
987 fn c() {
988 3
989 }
990 }
991 "
992 .unindent(),
993 cx,
994 );
995 build_editor(buffer, window, cx)
996 });
997
998 _ = editor.update(cx, |editor, window, cx| {
999 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1000 s.select_display_ranges([
1001 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1002 ]);
1003 });
1004 editor.fold(&Fold, window, cx);
1005 assert_eq!(
1006 editor.display_text(cx),
1007 "
1008 impl Foo {
1009 // Hello!
1010
1011 fn a() {
1012 1
1013 }
1014
1015 fn b() {⋯
1016 }
1017
1018 fn c() {⋯
1019 }
1020 }
1021 "
1022 .unindent(),
1023 );
1024
1025 editor.fold(&Fold, window, cx);
1026 assert_eq!(
1027 editor.display_text(cx),
1028 "
1029 impl Foo {⋯
1030 }
1031 "
1032 .unindent(),
1033 );
1034
1035 editor.unfold_lines(&UnfoldLines, window, cx);
1036 assert_eq!(
1037 editor.display_text(cx),
1038 "
1039 impl Foo {
1040 // Hello!
1041
1042 fn a() {
1043 1
1044 }
1045
1046 fn b() {⋯
1047 }
1048
1049 fn c() {⋯
1050 }
1051 }
1052 "
1053 .unindent(),
1054 );
1055
1056 editor.unfold_lines(&UnfoldLines, window, cx);
1057 assert_eq!(
1058 editor.display_text(cx),
1059 editor.buffer.read(cx).read(cx).text()
1060 );
1061 });
1062}
1063
1064#[gpui::test]
1065fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1066 init_test(cx, |_| {});
1067
1068 let editor = cx.add_window(|window, cx| {
1069 let buffer = MultiBuffer::build_simple(
1070 &"
1071 class Foo:
1072 # Hello!
1073
1074 def a():
1075 print(1)
1076
1077 def b():
1078 print(2)
1079
1080 def c():
1081 print(3)
1082 "
1083 .unindent(),
1084 cx,
1085 );
1086 build_editor(buffer, window, cx)
1087 });
1088
1089 _ = editor.update(cx, |editor, window, cx| {
1090 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1091 s.select_display_ranges([
1092 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1093 ]);
1094 });
1095 editor.fold(&Fold, window, cx);
1096 assert_eq!(
1097 editor.display_text(cx),
1098 "
1099 class Foo:
1100 # Hello!
1101
1102 def a():
1103 print(1)
1104
1105 def b():⋯
1106
1107 def c():⋯
1108 "
1109 .unindent(),
1110 );
1111
1112 editor.fold(&Fold, window, cx);
1113 assert_eq!(
1114 editor.display_text(cx),
1115 "
1116 class Foo:⋯
1117 "
1118 .unindent(),
1119 );
1120
1121 editor.unfold_lines(&UnfoldLines, window, cx);
1122 assert_eq!(
1123 editor.display_text(cx),
1124 "
1125 class Foo:
1126 # Hello!
1127
1128 def a():
1129 print(1)
1130
1131 def b():⋯
1132
1133 def c():⋯
1134 "
1135 .unindent(),
1136 );
1137
1138 editor.unfold_lines(&UnfoldLines, window, cx);
1139 assert_eq!(
1140 editor.display_text(cx),
1141 editor.buffer.read(cx).read(cx).text()
1142 );
1143 });
1144}
1145
1146#[gpui::test]
1147fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1148 init_test(cx, |_| {});
1149
1150 let editor = cx.add_window(|window, cx| {
1151 let buffer = MultiBuffer::build_simple(
1152 &"
1153 class Foo:
1154 # Hello!
1155
1156 def a():
1157 print(1)
1158
1159 def b():
1160 print(2)
1161
1162
1163 def c():
1164 print(3)
1165
1166
1167 "
1168 .unindent(),
1169 cx,
1170 );
1171 build_editor(buffer, window, cx)
1172 });
1173
1174 _ = editor.update(cx, |editor, window, cx| {
1175 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1176 s.select_display_ranges([
1177 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1178 ]);
1179 });
1180 editor.fold(&Fold, window, cx);
1181 assert_eq!(
1182 editor.display_text(cx),
1183 "
1184 class Foo:
1185 # Hello!
1186
1187 def a():
1188 print(1)
1189
1190 def b():⋯
1191
1192
1193 def c():⋯
1194
1195
1196 "
1197 .unindent(),
1198 );
1199
1200 editor.fold(&Fold, window, cx);
1201 assert_eq!(
1202 editor.display_text(cx),
1203 "
1204 class Foo:⋯
1205
1206
1207 "
1208 .unindent(),
1209 );
1210
1211 editor.unfold_lines(&UnfoldLines, window, cx);
1212 assert_eq!(
1213 editor.display_text(cx),
1214 "
1215 class Foo:
1216 # Hello!
1217
1218 def a():
1219 print(1)
1220
1221 def b():⋯
1222
1223
1224 def c():⋯
1225
1226
1227 "
1228 .unindent(),
1229 );
1230
1231 editor.unfold_lines(&UnfoldLines, window, cx);
1232 assert_eq!(
1233 editor.display_text(cx),
1234 editor.buffer.read(cx).read(cx).text()
1235 );
1236 });
1237}
1238
1239#[gpui::test]
1240fn test_fold_at_level(cx: &mut TestAppContext) {
1241 init_test(cx, |_| {});
1242
1243 let editor = cx.add_window(|window, cx| {
1244 let buffer = MultiBuffer::build_simple(
1245 &"
1246 class Foo:
1247 # Hello!
1248
1249 def a():
1250 print(1)
1251
1252 def b():
1253 print(2)
1254
1255
1256 class Bar:
1257 # World!
1258
1259 def a():
1260 print(1)
1261
1262 def b():
1263 print(2)
1264
1265
1266 "
1267 .unindent(),
1268 cx,
1269 );
1270 build_editor(buffer, window, cx)
1271 });
1272
1273 _ = editor.update(cx, |editor, window, cx| {
1274 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1275 assert_eq!(
1276 editor.display_text(cx),
1277 "
1278 class Foo:
1279 # Hello!
1280
1281 def a():⋯
1282
1283 def b():⋯
1284
1285
1286 class Bar:
1287 # World!
1288
1289 def a():⋯
1290
1291 def b():⋯
1292
1293
1294 "
1295 .unindent(),
1296 );
1297
1298 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1299 assert_eq!(
1300 editor.display_text(cx),
1301 "
1302 class Foo:⋯
1303
1304
1305 class Bar:⋯
1306
1307
1308 "
1309 .unindent(),
1310 );
1311
1312 editor.unfold_all(&UnfoldAll, window, cx);
1313 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1314 assert_eq!(
1315 editor.display_text(cx),
1316 "
1317 class Foo:
1318 # Hello!
1319
1320 def a():
1321 print(1)
1322
1323 def b():
1324 print(2)
1325
1326
1327 class Bar:
1328 # World!
1329
1330 def a():
1331 print(1)
1332
1333 def b():
1334 print(2)
1335
1336
1337 "
1338 .unindent(),
1339 );
1340
1341 assert_eq!(
1342 editor.display_text(cx),
1343 editor.buffer.read(cx).read(cx).text()
1344 );
1345 let (_, positions) = marked_text_ranges(
1346 &"
1347 class Foo:
1348 # Hello!
1349
1350 def a():
1351 print(1)
1352
1353 def b():
1354 p«riˇ»nt(2)
1355
1356
1357 class Bar:
1358 # World!
1359
1360 def a():
1361 «ˇprint(1)
1362
1363 def b():
1364 print(2)»
1365
1366
1367 "
1368 .unindent(),
1369 true,
1370 );
1371
1372 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1373 s.select_ranges(positions)
1374 });
1375
1376 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1377 assert_eq!(
1378 editor.display_text(cx),
1379 "
1380 class Foo:
1381 # Hello!
1382
1383 def a():⋯
1384
1385 def b():
1386 print(2)
1387
1388
1389 class Bar:
1390 # World!
1391
1392 def a():
1393 print(1)
1394
1395 def b():
1396 print(2)
1397
1398
1399 "
1400 .unindent(),
1401 );
1402 });
1403}
1404
1405#[gpui::test]
1406fn test_move_cursor(cx: &mut TestAppContext) {
1407 init_test(cx, |_| {});
1408
1409 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1410 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1411
1412 buffer.update(cx, |buffer, cx| {
1413 buffer.edit(
1414 vec![
1415 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1416 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1417 ],
1418 None,
1419 cx,
1420 );
1421 });
1422 _ = editor.update(cx, |editor, window, cx| {
1423 assert_eq!(
1424 editor.selections.display_ranges(cx),
1425 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1426 );
1427
1428 editor.move_down(&MoveDown, window, cx);
1429 assert_eq!(
1430 editor.selections.display_ranges(cx),
1431 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1432 );
1433
1434 editor.move_right(&MoveRight, window, cx);
1435 assert_eq!(
1436 editor.selections.display_ranges(cx),
1437 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1438 );
1439
1440 editor.move_left(&MoveLeft, window, cx);
1441 assert_eq!(
1442 editor.selections.display_ranges(cx),
1443 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1444 );
1445
1446 editor.move_up(&MoveUp, window, cx);
1447 assert_eq!(
1448 editor.selections.display_ranges(cx),
1449 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1450 );
1451
1452 editor.move_to_end(&MoveToEnd, window, cx);
1453 assert_eq!(
1454 editor.selections.display_ranges(cx),
1455 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1456 );
1457
1458 editor.move_to_beginning(&MoveToBeginning, window, cx);
1459 assert_eq!(
1460 editor.selections.display_ranges(cx),
1461 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1462 );
1463
1464 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1465 s.select_display_ranges([
1466 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1467 ]);
1468 });
1469 editor.select_to_beginning(&SelectToBeginning, window, cx);
1470 assert_eq!(
1471 editor.selections.display_ranges(cx),
1472 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1473 );
1474
1475 editor.select_to_end(&SelectToEnd, window, cx);
1476 assert_eq!(
1477 editor.selections.display_ranges(cx),
1478 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1479 );
1480 });
1481}
1482
1483#[gpui::test]
1484fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1485 init_test(cx, |_| {});
1486
1487 let editor = cx.add_window(|window, cx| {
1488 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1489 build_editor(buffer, window, cx)
1490 });
1491
1492 assert_eq!('🟥'.len_utf8(), 4);
1493 assert_eq!('α'.len_utf8(), 2);
1494
1495 _ = editor.update(cx, |editor, window, cx| {
1496 editor.fold_creases(
1497 vec![
1498 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1499 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1500 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1501 ],
1502 true,
1503 window,
1504 cx,
1505 );
1506 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1507
1508 editor.move_right(&MoveRight, window, cx);
1509 assert_eq!(
1510 editor.selections.display_ranges(cx),
1511 &[empty_range(0, "🟥".len())]
1512 );
1513 editor.move_right(&MoveRight, window, cx);
1514 assert_eq!(
1515 editor.selections.display_ranges(cx),
1516 &[empty_range(0, "🟥🟧".len())]
1517 );
1518 editor.move_right(&MoveRight, window, cx);
1519 assert_eq!(
1520 editor.selections.display_ranges(cx),
1521 &[empty_range(0, "🟥🟧⋯".len())]
1522 );
1523
1524 editor.move_down(&MoveDown, window, cx);
1525 assert_eq!(
1526 editor.selections.display_ranges(cx),
1527 &[empty_range(1, "ab⋯e".len())]
1528 );
1529 editor.move_left(&MoveLeft, window, cx);
1530 assert_eq!(
1531 editor.selections.display_ranges(cx),
1532 &[empty_range(1, "ab⋯".len())]
1533 );
1534 editor.move_left(&MoveLeft, window, cx);
1535 assert_eq!(
1536 editor.selections.display_ranges(cx),
1537 &[empty_range(1, "ab".len())]
1538 );
1539 editor.move_left(&MoveLeft, window, cx);
1540 assert_eq!(
1541 editor.selections.display_ranges(cx),
1542 &[empty_range(1, "a".len())]
1543 );
1544
1545 editor.move_down(&MoveDown, window, cx);
1546 assert_eq!(
1547 editor.selections.display_ranges(cx),
1548 &[empty_range(2, "α".len())]
1549 );
1550 editor.move_right(&MoveRight, window, cx);
1551 assert_eq!(
1552 editor.selections.display_ranges(cx),
1553 &[empty_range(2, "αβ".len())]
1554 );
1555 editor.move_right(&MoveRight, window, cx);
1556 assert_eq!(
1557 editor.selections.display_ranges(cx),
1558 &[empty_range(2, "αβ⋯".len())]
1559 );
1560 editor.move_right(&MoveRight, window, cx);
1561 assert_eq!(
1562 editor.selections.display_ranges(cx),
1563 &[empty_range(2, "αβ⋯ε".len())]
1564 );
1565
1566 editor.move_up(&MoveUp, window, cx);
1567 assert_eq!(
1568 editor.selections.display_ranges(cx),
1569 &[empty_range(1, "ab⋯e".len())]
1570 );
1571 editor.move_down(&MoveDown, window, cx);
1572 assert_eq!(
1573 editor.selections.display_ranges(cx),
1574 &[empty_range(2, "αβ⋯ε".len())]
1575 );
1576 editor.move_up(&MoveUp, window, cx);
1577 assert_eq!(
1578 editor.selections.display_ranges(cx),
1579 &[empty_range(1, "ab⋯e".len())]
1580 );
1581
1582 editor.move_up(&MoveUp, window, cx);
1583 assert_eq!(
1584 editor.selections.display_ranges(cx),
1585 &[empty_range(0, "🟥🟧".len())]
1586 );
1587 editor.move_left(&MoveLeft, window, cx);
1588 assert_eq!(
1589 editor.selections.display_ranges(cx),
1590 &[empty_range(0, "🟥".len())]
1591 );
1592 editor.move_left(&MoveLeft, window, cx);
1593 assert_eq!(
1594 editor.selections.display_ranges(cx),
1595 &[empty_range(0, "".len())]
1596 );
1597 });
1598}
1599
1600#[gpui::test]
1601fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1602 init_test(cx, |_| {});
1603
1604 let editor = cx.add_window(|window, cx| {
1605 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1606 build_editor(buffer, window, cx)
1607 });
1608 _ = editor.update(cx, |editor, window, cx| {
1609 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1610 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1611 });
1612
1613 // moving above start of document should move selection to start of document,
1614 // but the next move down should still be at the original goal_x
1615 editor.move_up(&MoveUp, window, cx);
1616 assert_eq!(
1617 editor.selections.display_ranges(cx),
1618 &[empty_range(0, "".len())]
1619 );
1620
1621 editor.move_down(&MoveDown, window, cx);
1622 assert_eq!(
1623 editor.selections.display_ranges(cx),
1624 &[empty_range(1, "abcd".len())]
1625 );
1626
1627 editor.move_down(&MoveDown, window, cx);
1628 assert_eq!(
1629 editor.selections.display_ranges(cx),
1630 &[empty_range(2, "αβγ".len())]
1631 );
1632
1633 editor.move_down(&MoveDown, window, cx);
1634 assert_eq!(
1635 editor.selections.display_ranges(cx),
1636 &[empty_range(3, "abcd".len())]
1637 );
1638
1639 editor.move_down(&MoveDown, window, cx);
1640 assert_eq!(
1641 editor.selections.display_ranges(cx),
1642 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1643 );
1644
1645 // moving past end of document should not change goal_x
1646 editor.move_down(&MoveDown, window, cx);
1647 assert_eq!(
1648 editor.selections.display_ranges(cx),
1649 &[empty_range(5, "".len())]
1650 );
1651
1652 editor.move_down(&MoveDown, window, cx);
1653 assert_eq!(
1654 editor.selections.display_ranges(cx),
1655 &[empty_range(5, "".len())]
1656 );
1657
1658 editor.move_up(&MoveUp, window, cx);
1659 assert_eq!(
1660 editor.selections.display_ranges(cx),
1661 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1662 );
1663
1664 editor.move_up(&MoveUp, window, cx);
1665 assert_eq!(
1666 editor.selections.display_ranges(cx),
1667 &[empty_range(3, "abcd".len())]
1668 );
1669
1670 editor.move_up(&MoveUp, window, cx);
1671 assert_eq!(
1672 editor.selections.display_ranges(cx),
1673 &[empty_range(2, "αβγ".len())]
1674 );
1675 });
1676}
1677
1678#[gpui::test]
1679fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1680 init_test(cx, |_| {});
1681 let move_to_beg = MoveToBeginningOfLine {
1682 stop_at_soft_wraps: true,
1683 stop_at_indent: true,
1684 };
1685
1686 let delete_to_beg = DeleteToBeginningOfLine {
1687 stop_at_indent: false,
1688 };
1689
1690 let move_to_end = MoveToEndOfLine {
1691 stop_at_soft_wraps: true,
1692 };
1693
1694 let editor = cx.add_window(|window, cx| {
1695 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1696 build_editor(buffer, window, cx)
1697 });
1698 _ = editor.update(cx, |editor, window, cx| {
1699 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1700 s.select_display_ranges([
1701 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1702 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1703 ]);
1704 });
1705 });
1706
1707 _ = editor.update(cx, |editor, window, cx| {
1708 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1709 assert_eq!(
1710 editor.selections.display_ranges(cx),
1711 &[
1712 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1713 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1714 ]
1715 );
1716 });
1717
1718 _ = editor.update(cx, |editor, window, cx| {
1719 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1720 assert_eq!(
1721 editor.selections.display_ranges(cx),
1722 &[
1723 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1724 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1725 ]
1726 );
1727 });
1728
1729 _ = editor.update(cx, |editor, window, cx| {
1730 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1731 assert_eq!(
1732 editor.selections.display_ranges(cx),
1733 &[
1734 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1735 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1736 ]
1737 );
1738 });
1739
1740 _ = editor.update(cx, |editor, window, cx| {
1741 editor.move_to_end_of_line(&move_to_end, window, cx);
1742 assert_eq!(
1743 editor.selections.display_ranges(cx),
1744 &[
1745 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1746 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1747 ]
1748 );
1749 });
1750
1751 // Moving to the end of line again is a no-op.
1752 _ = editor.update(cx, |editor, window, cx| {
1753 editor.move_to_end_of_line(&move_to_end, window, cx);
1754 assert_eq!(
1755 editor.selections.display_ranges(cx),
1756 &[
1757 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1758 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1759 ]
1760 );
1761 });
1762
1763 _ = editor.update(cx, |editor, window, cx| {
1764 editor.move_left(&MoveLeft, window, cx);
1765 editor.select_to_beginning_of_line(
1766 &SelectToBeginningOfLine {
1767 stop_at_soft_wraps: true,
1768 stop_at_indent: true,
1769 },
1770 window,
1771 cx,
1772 );
1773 assert_eq!(
1774 editor.selections.display_ranges(cx),
1775 &[
1776 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1777 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1778 ]
1779 );
1780 });
1781
1782 _ = editor.update(cx, |editor, window, cx| {
1783 editor.select_to_beginning_of_line(
1784 &SelectToBeginningOfLine {
1785 stop_at_soft_wraps: true,
1786 stop_at_indent: true,
1787 },
1788 window,
1789 cx,
1790 );
1791 assert_eq!(
1792 editor.selections.display_ranges(cx),
1793 &[
1794 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1795 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1796 ]
1797 );
1798 });
1799
1800 _ = editor.update(cx, |editor, window, cx| {
1801 editor.select_to_beginning_of_line(
1802 &SelectToBeginningOfLine {
1803 stop_at_soft_wraps: true,
1804 stop_at_indent: true,
1805 },
1806 window,
1807 cx,
1808 );
1809 assert_eq!(
1810 editor.selections.display_ranges(cx),
1811 &[
1812 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1813 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1814 ]
1815 );
1816 });
1817
1818 _ = editor.update(cx, |editor, window, cx| {
1819 editor.select_to_end_of_line(
1820 &SelectToEndOfLine {
1821 stop_at_soft_wraps: true,
1822 },
1823 window,
1824 cx,
1825 );
1826 assert_eq!(
1827 editor.selections.display_ranges(cx),
1828 &[
1829 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1830 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1831 ]
1832 );
1833 });
1834
1835 _ = editor.update(cx, |editor, window, cx| {
1836 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1837 assert_eq!(editor.display_text(cx), "ab\n de");
1838 assert_eq!(
1839 editor.selections.display_ranges(cx),
1840 &[
1841 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1842 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1843 ]
1844 );
1845 });
1846
1847 _ = editor.update(cx, |editor, window, cx| {
1848 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1849 assert_eq!(editor.display_text(cx), "\n");
1850 assert_eq!(
1851 editor.selections.display_ranges(cx),
1852 &[
1853 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1854 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1855 ]
1856 );
1857 });
1858}
1859
1860#[gpui::test]
1861fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1862 init_test(cx, |_| {});
1863 let move_to_beg = MoveToBeginningOfLine {
1864 stop_at_soft_wraps: false,
1865 stop_at_indent: false,
1866 };
1867
1868 let move_to_end = MoveToEndOfLine {
1869 stop_at_soft_wraps: false,
1870 };
1871
1872 let editor = cx.add_window(|window, cx| {
1873 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1874 build_editor(buffer, window, cx)
1875 });
1876
1877 _ = editor.update(cx, |editor, window, cx| {
1878 editor.set_wrap_width(Some(140.0.into()), cx);
1879
1880 // We expect the following lines after wrapping
1881 // ```
1882 // thequickbrownfox
1883 // jumpedoverthelazydo
1884 // gs
1885 // ```
1886 // The final `gs` was soft-wrapped onto a new line.
1887 assert_eq!(
1888 "thequickbrownfox\njumpedoverthelaz\nydogs",
1889 editor.display_text(cx),
1890 );
1891
1892 // First, let's assert behavior on the first line, that was not soft-wrapped.
1893 // Start the cursor at the `k` on the first line
1894 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1895 s.select_display_ranges([
1896 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1897 ]);
1898 });
1899
1900 // Moving to the beginning of the line should put us at the beginning of the line.
1901 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1902 assert_eq!(
1903 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1904 editor.selections.display_ranges(cx)
1905 );
1906
1907 // Moving to the end of the line should put us at the end of the line.
1908 editor.move_to_end_of_line(&move_to_end, window, cx);
1909 assert_eq!(
1910 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1911 editor.selections.display_ranges(cx)
1912 );
1913
1914 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1915 // Start the cursor at the last line (`y` that was wrapped to a new line)
1916 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1917 s.select_display_ranges([
1918 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1919 ]);
1920 });
1921
1922 // Moving to the beginning of the line should put us at the start of the second line of
1923 // display text, i.e., the `j`.
1924 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1925 assert_eq!(
1926 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1927 editor.selections.display_ranges(cx)
1928 );
1929
1930 // Moving to the beginning of the line again should be a no-op.
1931 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1932 assert_eq!(
1933 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1934 editor.selections.display_ranges(cx)
1935 );
1936
1937 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1938 // next display line.
1939 editor.move_to_end_of_line(&move_to_end, window, cx);
1940 assert_eq!(
1941 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1942 editor.selections.display_ranges(cx)
1943 );
1944
1945 // Moving to the end of the line again should be a no-op.
1946 editor.move_to_end_of_line(&move_to_end, window, cx);
1947 assert_eq!(
1948 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1949 editor.selections.display_ranges(cx)
1950 );
1951 });
1952}
1953
1954#[gpui::test]
1955fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1956 init_test(cx, |_| {});
1957
1958 let move_to_beg = MoveToBeginningOfLine {
1959 stop_at_soft_wraps: true,
1960 stop_at_indent: true,
1961 };
1962
1963 let select_to_beg = SelectToBeginningOfLine {
1964 stop_at_soft_wraps: true,
1965 stop_at_indent: true,
1966 };
1967
1968 let delete_to_beg = DeleteToBeginningOfLine {
1969 stop_at_indent: true,
1970 };
1971
1972 let move_to_end = MoveToEndOfLine {
1973 stop_at_soft_wraps: false,
1974 };
1975
1976 let editor = cx.add_window(|window, cx| {
1977 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1978 build_editor(buffer, window, cx)
1979 });
1980
1981 _ = editor.update(cx, |editor, window, cx| {
1982 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1983 s.select_display_ranges([
1984 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1985 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1986 ]);
1987 });
1988
1989 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1990 // and the second cursor at the first non-whitespace character in the line.
1991 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1992 assert_eq!(
1993 editor.selections.display_ranges(cx),
1994 &[
1995 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1996 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1997 ]
1998 );
1999
2000 // Moving to the beginning of the line again should be a no-op for the first cursor,
2001 // and should move the second cursor to the beginning of the line.
2002 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2003 assert_eq!(
2004 editor.selections.display_ranges(cx),
2005 &[
2006 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2007 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
2008 ]
2009 );
2010
2011 // Moving to the beginning of the line again should still be a no-op for the first cursor,
2012 // and should move the second cursor back to the first non-whitespace character in the line.
2013 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2014 assert_eq!(
2015 editor.selections.display_ranges(cx),
2016 &[
2017 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2018 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2019 ]
2020 );
2021
2022 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
2023 // and to the first non-whitespace character in the line for the second cursor.
2024 editor.move_to_end_of_line(&move_to_end, window, cx);
2025 editor.move_left(&MoveLeft, window, cx);
2026 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2027 assert_eq!(
2028 editor.selections.display_ranges(cx),
2029 &[
2030 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2031 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2032 ]
2033 );
2034
2035 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2036 // and should select to the beginning of the line for the second cursor.
2037 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2038 assert_eq!(
2039 editor.selections.display_ranges(cx),
2040 &[
2041 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2042 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2043 ]
2044 );
2045
2046 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2047 // and should delete to the first non-whitespace character in the line for the second cursor.
2048 editor.move_to_end_of_line(&move_to_end, window, cx);
2049 editor.move_left(&MoveLeft, window, cx);
2050 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2051 assert_eq!(editor.text(cx), "c\n f");
2052 });
2053}
2054
2055#[gpui::test]
2056fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2057 init_test(cx, |_| {});
2058
2059 let move_to_beg = MoveToBeginningOfLine {
2060 stop_at_soft_wraps: true,
2061 stop_at_indent: true,
2062 };
2063
2064 let editor = cx.add_window(|window, cx| {
2065 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2066 build_editor(buffer, window, cx)
2067 });
2068
2069 _ = editor.update(cx, |editor, window, cx| {
2070 // test cursor between line_start and indent_start
2071 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2072 s.select_display_ranges([
2073 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2074 ]);
2075 });
2076
2077 // cursor should move to line_start
2078 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2079 assert_eq!(
2080 editor.selections.display_ranges(cx),
2081 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2082 );
2083
2084 // cursor should move to indent_start
2085 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2086 assert_eq!(
2087 editor.selections.display_ranges(cx),
2088 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2089 );
2090
2091 // cursor should move to back to line_start
2092 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2093 assert_eq!(
2094 editor.selections.display_ranges(cx),
2095 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2096 );
2097 });
2098}
2099
2100#[gpui::test]
2101fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2102 init_test(cx, |_| {});
2103
2104 let editor = cx.add_window(|window, cx| {
2105 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2106 build_editor(buffer, window, cx)
2107 });
2108 _ = editor.update(cx, |editor, window, cx| {
2109 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2110 s.select_display_ranges([
2111 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2112 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2113 ])
2114 });
2115 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2116 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2117
2118 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2119 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2120
2121 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2122 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2123
2124 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2125 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2126
2127 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2128 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2129
2130 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2131 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2132
2133 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2134 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2135
2136 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2137 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2138
2139 editor.move_right(&MoveRight, window, cx);
2140 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2141 assert_selection_ranges(
2142 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2143 editor,
2144 cx,
2145 );
2146
2147 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2148 assert_selection_ranges(
2149 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2150 editor,
2151 cx,
2152 );
2153
2154 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2155 assert_selection_ranges(
2156 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2157 editor,
2158 cx,
2159 );
2160 });
2161}
2162
2163#[gpui::test]
2164fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2165 init_test(cx, |_| {});
2166
2167 let editor = cx.add_window(|window, cx| {
2168 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2169 build_editor(buffer, window, cx)
2170 });
2171
2172 _ = editor.update(cx, |editor, window, cx| {
2173 editor.set_wrap_width(Some(140.0.into()), cx);
2174 assert_eq!(
2175 editor.display_text(cx),
2176 "use one::{\n two::three::\n four::five\n};"
2177 );
2178
2179 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2180 s.select_display_ranges([
2181 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2182 ]);
2183 });
2184
2185 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2186 assert_eq!(
2187 editor.selections.display_ranges(cx),
2188 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2189 );
2190
2191 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2192 assert_eq!(
2193 editor.selections.display_ranges(cx),
2194 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2195 );
2196
2197 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2198 assert_eq!(
2199 editor.selections.display_ranges(cx),
2200 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2201 );
2202
2203 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2204 assert_eq!(
2205 editor.selections.display_ranges(cx),
2206 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2207 );
2208
2209 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2210 assert_eq!(
2211 editor.selections.display_ranges(cx),
2212 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2213 );
2214
2215 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2216 assert_eq!(
2217 editor.selections.display_ranges(cx),
2218 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2219 );
2220 });
2221}
2222
2223#[gpui::test]
2224async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2225 init_test(cx, |_| {});
2226 let mut cx = EditorTestContext::new(cx).await;
2227
2228 let line_height = cx.editor(|editor, window, _| {
2229 editor
2230 .style()
2231 .unwrap()
2232 .text
2233 .line_height_in_pixels(window.rem_size())
2234 });
2235 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2236
2237 cx.set_state(
2238 &r#"ˇone
2239 two
2240
2241 three
2242 fourˇ
2243 five
2244
2245 six"#
2246 .unindent(),
2247 );
2248
2249 cx.update_editor(|editor, window, cx| {
2250 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2251 });
2252 cx.assert_editor_state(
2253 &r#"one
2254 two
2255 ˇ
2256 three
2257 four
2258 five
2259 ˇ
2260 six"#
2261 .unindent(),
2262 );
2263
2264 cx.update_editor(|editor, window, cx| {
2265 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2266 });
2267 cx.assert_editor_state(
2268 &r#"one
2269 two
2270
2271 three
2272 four
2273 five
2274 ˇ
2275 sixˇ"#
2276 .unindent(),
2277 );
2278
2279 cx.update_editor(|editor, window, cx| {
2280 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2281 });
2282 cx.assert_editor_state(
2283 &r#"one
2284 two
2285
2286 three
2287 four
2288 five
2289
2290 sixˇ"#
2291 .unindent(),
2292 );
2293
2294 cx.update_editor(|editor, window, cx| {
2295 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2296 });
2297 cx.assert_editor_state(
2298 &r#"one
2299 two
2300
2301 three
2302 four
2303 five
2304 ˇ
2305 six"#
2306 .unindent(),
2307 );
2308
2309 cx.update_editor(|editor, window, cx| {
2310 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2311 });
2312 cx.assert_editor_state(
2313 &r#"one
2314 two
2315 ˇ
2316 three
2317 four
2318 five
2319
2320 six"#
2321 .unindent(),
2322 );
2323
2324 cx.update_editor(|editor, window, cx| {
2325 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2326 });
2327 cx.assert_editor_state(
2328 &r#"ˇone
2329 two
2330
2331 three
2332 four
2333 five
2334
2335 six"#
2336 .unindent(),
2337 );
2338}
2339
2340#[gpui::test]
2341async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2342 init_test(cx, |_| {});
2343 let mut cx = EditorTestContext::new(cx).await;
2344 let line_height = cx.editor(|editor, window, _| {
2345 editor
2346 .style()
2347 .unwrap()
2348 .text
2349 .line_height_in_pixels(window.rem_size())
2350 });
2351 let window = cx.window;
2352 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2353
2354 cx.set_state(
2355 r#"ˇone
2356 two
2357 three
2358 four
2359 five
2360 six
2361 seven
2362 eight
2363 nine
2364 ten
2365 "#,
2366 );
2367
2368 cx.update_editor(|editor, window, cx| {
2369 assert_eq!(
2370 editor.snapshot(window, cx).scroll_position(),
2371 gpui::Point::new(0., 0.)
2372 );
2373 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2374 assert_eq!(
2375 editor.snapshot(window, cx).scroll_position(),
2376 gpui::Point::new(0., 3.)
2377 );
2378 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2379 assert_eq!(
2380 editor.snapshot(window, cx).scroll_position(),
2381 gpui::Point::new(0., 6.)
2382 );
2383 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2384 assert_eq!(
2385 editor.snapshot(window, cx).scroll_position(),
2386 gpui::Point::new(0., 3.)
2387 );
2388
2389 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2390 assert_eq!(
2391 editor.snapshot(window, cx).scroll_position(),
2392 gpui::Point::new(0., 1.)
2393 );
2394 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2395 assert_eq!(
2396 editor.snapshot(window, cx).scroll_position(),
2397 gpui::Point::new(0., 3.)
2398 );
2399 });
2400}
2401
2402#[gpui::test]
2403async fn test_autoscroll(cx: &mut TestAppContext) {
2404 init_test(cx, |_| {});
2405 let mut cx = EditorTestContext::new(cx).await;
2406
2407 let line_height = cx.update_editor(|editor, window, cx| {
2408 editor.set_vertical_scroll_margin(2, cx);
2409 editor
2410 .style()
2411 .unwrap()
2412 .text
2413 .line_height_in_pixels(window.rem_size())
2414 });
2415 let window = cx.window;
2416 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2417
2418 cx.set_state(
2419 r#"ˇone
2420 two
2421 three
2422 four
2423 five
2424 six
2425 seven
2426 eight
2427 nine
2428 ten
2429 "#,
2430 );
2431 cx.update_editor(|editor, window, cx| {
2432 assert_eq!(
2433 editor.snapshot(window, cx).scroll_position(),
2434 gpui::Point::new(0., 0.0)
2435 );
2436 });
2437
2438 // Add a cursor below the visible area. Since both cursors cannot fit
2439 // on screen, the editor autoscrolls to reveal the newest cursor, and
2440 // allows the vertical scroll margin below that cursor.
2441 cx.update_editor(|editor, window, cx| {
2442 editor.change_selections(Default::default(), window, cx, |selections| {
2443 selections.select_ranges([
2444 Point::new(0, 0)..Point::new(0, 0),
2445 Point::new(6, 0)..Point::new(6, 0),
2446 ]);
2447 })
2448 });
2449 cx.update_editor(|editor, window, cx| {
2450 assert_eq!(
2451 editor.snapshot(window, cx).scroll_position(),
2452 gpui::Point::new(0., 3.0)
2453 );
2454 });
2455
2456 // Move down. The editor cursor scrolls down to track the newest cursor.
2457 cx.update_editor(|editor, window, cx| {
2458 editor.move_down(&Default::default(), window, cx);
2459 });
2460 cx.update_editor(|editor, window, cx| {
2461 assert_eq!(
2462 editor.snapshot(window, cx).scroll_position(),
2463 gpui::Point::new(0., 4.0)
2464 );
2465 });
2466
2467 // Add a cursor above the visible area. Since both cursors fit on screen,
2468 // the editor scrolls to show both.
2469 cx.update_editor(|editor, window, cx| {
2470 editor.change_selections(Default::default(), window, cx, |selections| {
2471 selections.select_ranges([
2472 Point::new(1, 0)..Point::new(1, 0),
2473 Point::new(6, 0)..Point::new(6, 0),
2474 ]);
2475 })
2476 });
2477 cx.update_editor(|editor, window, cx| {
2478 assert_eq!(
2479 editor.snapshot(window, cx).scroll_position(),
2480 gpui::Point::new(0., 1.0)
2481 );
2482 });
2483}
2484
2485#[gpui::test]
2486async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2487 init_test(cx, |_| {});
2488 let mut cx = EditorTestContext::new(cx).await;
2489
2490 let line_height = cx.editor(|editor, window, _cx| {
2491 editor
2492 .style()
2493 .unwrap()
2494 .text
2495 .line_height_in_pixels(window.rem_size())
2496 });
2497 let window = cx.window;
2498 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2499 cx.set_state(
2500 &r#"
2501 ˇone
2502 two
2503 threeˇ
2504 four
2505 five
2506 six
2507 seven
2508 eight
2509 nine
2510 ten
2511 "#
2512 .unindent(),
2513 );
2514
2515 cx.update_editor(|editor, window, cx| {
2516 editor.move_page_down(&MovePageDown::default(), window, cx)
2517 });
2518 cx.assert_editor_state(
2519 &r#"
2520 one
2521 two
2522 three
2523 ˇfour
2524 five
2525 sixˇ
2526 seven
2527 eight
2528 nine
2529 ten
2530 "#
2531 .unindent(),
2532 );
2533
2534 cx.update_editor(|editor, window, cx| {
2535 editor.move_page_down(&MovePageDown::default(), window, cx)
2536 });
2537 cx.assert_editor_state(
2538 &r#"
2539 one
2540 two
2541 three
2542 four
2543 five
2544 six
2545 ˇseven
2546 eight
2547 nineˇ
2548 ten
2549 "#
2550 .unindent(),
2551 );
2552
2553 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2554 cx.assert_editor_state(
2555 &r#"
2556 one
2557 two
2558 three
2559 ˇfour
2560 five
2561 sixˇ
2562 seven
2563 eight
2564 nine
2565 ten
2566 "#
2567 .unindent(),
2568 );
2569
2570 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2571 cx.assert_editor_state(
2572 &r#"
2573 ˇone
2574 two
2575 threeˇ
2576 four
2577 five
2578 six
2579 seven
2580 eight
2581 nine
2582 ten
2583 "#
2584 .unindent(),
2585 );
2586
2587 // Test select collapsing
2588 cx.update_editor(|editor, window, cx| {
2589 editor.move_page_down(&MovePageDown::default(), window, cx);
2590 editor.move_page_down(&MovePageDown::default(), window, cx);
2591 editor.move_page_down(&MovePageDown::default(), window, cx);
2592 });
2593 cx.assert_editor_state(
2594 &r#"
2595 one
2596 two
2597 three
2598 four
2599 five
2600 six
2601 seven
2602 eight
2603 nine
2604 ˇten
2605 ˇ"#
2606 .unindent(),
2607 );
2608}
2609
2610#[gpui::test]
2611async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2612 init_test(cx, |_| {});
2613 let mut cx = EditorTestContext::new(cx).await;
2614 cx.set_state("one «two threeˇ» four");
2615 cx.update_editor(|editor, window, cx| {
2616 editor.delete_to_beginning_of_line(
2617 &DeleteToBeginningOfLine {
2618 stop_at_indent: false,
2619 },
2620 window,
2621 cx,
2622 );
2623 assert_eq!(editor.text(cx), " four");
2624 });
2625}
2626
2627#[gpui::test]
2628async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2629 init_test(cx, |_| {});
2630
2631 let mut cx = EditorTestContext::new(cx).await;
2632
2633 // For an empty selection, the preceding word fragment is deleted.
2634 // For non-empty selections, only selected characters are deleted.
2635 cx.set_state("onˇe two t«hreˇ»e four");
2636 cx.update_editor(|editor, window, cx| {
2637 editor.delete_to_previous_word_start(
2638 &DeleteToPreviousWordStart {
2639 ignore_newlines: false,
2640 ignore_brackets: false,
2641 },
2642 window,
2643 cx,
2644 );
2645 });
2646 cx.assert_editor_state("ˇe two tˇe four");
2647
2648 cx.set_state("e tˇwo te «fˇ»our");
2649 cx.update_editor(|editor, window, cx| {
2650 editor.delete_to_next_word_end(
2651 &DeleteToNextWordEnd {
2652 ignore_newlines: false,
2653 ignore_brackets: false,
2654 },
2655 window,
2656 cx,
2657 );
2658 });
2659 cx.assert_editor_state("e tˇ te ˇour");
2660}
2661
2662#[gpui::test]
2663async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2664 init_test(cx, |_| {});
2665
2666 let mut cx = EditorTestContext::new(cx).await;
2667
2668 cx.set_state("here is some text ˇwith a space");
2669 cx.update_editor(|editor, window, cx| {
2670 editor.delete_to_previous_word_start(
2671 &DeleteToPreviousWordStart {
2672 ignore_newlines: false,
2673 ignore_brackets: true,
2674 },
2675 window,
2676 cx,
2677 );
2678 });
2679 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2680 cx.assert_editor_state("here is some textˇwith a space");
2681
2682 cx.set_state("here is some text ˇwith a space");
2683 cx.update_editor(|editor, window, cx| {
2684 editor.delete_to_previous_word_start(
2685 &DeleteToPreviousWordStart {
2686 ignore_newlines: false,
2687 ignore_brackets: false,
2688 },
2689 window,
2690 cx,
2691 );
2692 });
2693 cx.assert_editor_state("here is some textˇwith a space");
2694
2695 cx.set_state("here is some textˇ with a space");
2696 cx.update_editor(|editor, window, cx| {
2697 editor.delete_to_next_word_end(
2698 &DeleteToNextWordEnd {
2699 ignore_newlines: false,
2700 ignore_brackets: true,
2701 },
2702 window,
2703 cx,
2704 );
2705 });
2706 // Same happens in the other direction.
2707 cx.assert_editor_state("here is some textˇwith a space");
2708
2709 cx.set_state("here is some textˇ with a space");
2710 cx.update_editor(|editor, window, cx| {
2711 editor.delete_to_next_word_end(
2712 &DeleteToNextWordEnd {
2713 ignore_newlines: false,
2714 ignore_brackets: false,
2715 },
2716 window,
2717 cx,
2718 );
2719 });
2720 cx.assert_editor_state("here is some textˇwith a space");
2721
2722 cx.set_state("here is some textˇ with a space");
2723 cx.update_editor(|editor, window, cx| {
2724 editor.delete_to_next_word_end(
2725 &DeleteToNextWordEnd {
2726 ignore_newlines: true,
2727 ignore_brackets: false,
2728 },
2729 window,
2730 cx,
2731 );
2732 });
2733 cx.assert_editor_state("here is some textˇwith a space");
2734 cx.update_editor(|editor, window, cx| {
2735 editor.delete_to_previous_word_start(
2736 &DeleteToPreviousWordStart {
2737 ignore_newlines: true,
2738 ignore_brackets: false,
2739 },
2740 window,
2741 cx,
2742 );
2743 });
2744 cx.assert_editor_state("here is some ˇwith a space");
2745 cx.update_editor(|editor, window, cx| {
2746 editor.delete_to_previous_word_start(
2747 &DeleteToPreviousWordStart {
2748 ignore_newlines: true,
2749 ignore_brackets: false,
2750 },
2751 window,
2752 cx,
2753 );
2754 });
2755 // Single whitespaces are removed with the word behind them.
2756 cx.assert_editor_state("here is ˇwith a space");
2757 cx.update_editor(|editor, window, cx| {
2758 editor.delete_to_previous_word_start(
2759 &DeleteToPreviousWordStart {
2760 ignore_newlines: true,
2761 ignore_brackets: false,
2762 },
2763 window,
2764 cx,
2765 );
2766 });
2767 cx.assert_editor_state("here ˇwith a space");
2768 cx.update_editor(|editor, window, cx| {
2769 editor.delete_to_previous_word_start(
2770 &DeleteToPreviousWordStart {
2771 ignore_newlines: true,
2772 ignore_brackets: false,
2773 },
2774 window,
2775 cx,
2776 );
2777 });
2778 cx.assert_editor_state("ˇwith a space");
2779 cx.update_editor(|editor, window, cx| {
2780 editor.delete_to_previous_word_start(
2781 &DeleteToPreviousWordStart {
2782 ignore_newlines: true,
2783 ignore_brackets: false,
2784 },
2785 window,
2786 cx,
2787 );
2788 });
2789 cx.assert_editor_state("ˇwith a space");
2790 cx.update_editor(|editor, window, cx| {
2791 editor.delete_to_next_word_end(
2792 &DeleteToNextWordEnd {
2793 ignore_newlines: true,
2794 ignore_brackets: false,
2795 },
2796 window,
2797 cx,
2798 );
2799 });
2800 // Same happens in the other direction.
2801 cx.assert_editor_state("ˇ a space");
2802 cx.update_editor(|editor, window, cx| {
2803 editor.delete_to_next_word_end(
2804 &DeleteToNextWordEnd {
2805 ignore_newlines: true,
2806 ignore_brackets: false,
2807 },
2808 window,
2809 cx,
2810 );
2811 });
2812 cx.assert_editor_state("ˇ space");
2813 cx.update_editor(|editor, window, cx| {
2814 editor.delete_to_next_word_end(
2815 &DeleteToNextWordEnd {
2816 ignore_newlines: true,
2817 ignore_brackets: false,
2818 },
2819 window,
2820 cx,
2821 );
2822 });
2823 cx.assert_editor_state("ˇ");
2824 cx.update_editor(|editor, window, cx| {
2825 editor.delete_to_next_word_end(
2826 &DeleteToNextWordEnd {
2827 ignore_newlines: true,
2828 ignore_brackets: false,
2829 },
2830 window,
2831 cx,
2832 );
2833 });
2834 cx.assert_editor_state("ˇ");
2835 cx.update_editor(|editor, window, cx| {
2836 editor.delete_to_previous_word_start(
2837 &DeleteToPreviousWordStart {
2838 ignore_newlines: true,
2839 ignore_brackets: false,
2840 },
2841 window,
2842 cx,
2843 );
2844 });
2845 cx.assert_editor_state("ˇ");
2846}
2847
2848#[gpui::test]
2849async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2850 init_test(cx, |_| {});
2851
2852 let language = Arc::new(
2853 Language::new(
2854 LanguageConfig {
2855 brackets: BracketPairConfig {
2856 pairs: vec![
2857 BracketPair {
2858 start: "\"".to_string(),
2859 end: "\"".to_string(),
2860 close: true,
2861 surround: true,
2862 newline: false,
2863 },
2864 BracketPair {
2865 start: "(".to_string(),
2866 end: ")".to_string(),
2867 close: true,
2868 surround: true,
2869 newline: true,
2870 },
2871 ],
2872 ..BracketPairConfig::default()
2873 },
2874 ..LanguageConfig::default()
2875 },
2876 Some(tree_sitter_rust::LANGUAGE.into()),
2877 )
2878 .with_brackets_query(
2879 r#"
2880 ("(" @open ")" @close)
2881 ("\"" @open "\"" @close)
2882 "#,
2883 )
2884 .unwrap(),
2885 );
2886
2887 let mut cx = EditorTestContext::new(cx).await;
2888 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2889
2890 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2891 cx.update_editor(|editor, window, cx| {
2892 editor.delete_to_previous_word_start(
2893 &DeleteToPreviousWordStart {
2894 ignore_newlines: true,
2895 ignore_brackets: false,
2896 },
2897 window,
2898 cx,
2899 );
2900 });
2901 // Deletion stops before brackets if asked to not ignore them.
2902 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2903 cx.update_editor(|editor, window, cx| {
2904 editor.delete_to_previous_word_start(
2905 &DeleteToPreviousWordStart {
2906 ignore_newlines: true,
2907 ignore_brackets: false,
2908 },
2909 window,
2910 cx,
2911 );
2912 });
2913 // Deletion has to remove a single bracket and then stop again.
2914 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2915
2916 cx.update_editor(|editor, window, cx| {
2917 editor.delete_to_previous_word_start(
2918 &DeleteToPreviousWordStart {
2919 ignore_newlines: true,
2920 ignore_brackets: false,
2921 },
2922 window,
2923 cx,
2924 );
2925 });
2926 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2927
2928 cx.update_editor(|editor, window, cx| {
2929 editor.delete_to_previous_word_start(
2930 &DeleteToPreviousWordStart {
2931 ignore_newlines: true,
2932 ignore_brackets: false,
2933 },
2934 window,
2935 cx,
2936 );
2937 });
2938 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2939
2940 cx.update_editor(|editor, window, cx| {
2941 editor.delete_to_previous_word_start(
2942 &DeleteToPreviousWordStart {
2943 ignore_newlines: true,
2944 ignore_brackets: false,
2945 },
2946 window,
2947 cx,
2948 );
2949 });
2950 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2951
2952 cx.update_editor(|editor, window, cx| {
2953 editor.delete_to_next_word_end(
2954 &DeleteToNextWordEnd {
2955 ignore_newlines: true,
2956 ignore_brackets: false,
2957 },
2958 window,
2959 cx,
2960 );
2961 });
2962 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2963 cx.assert_editor_state(r#"ˇ");"#);
2964
2965 cx.update_editor(|editor, window, cx| {
2966 editor.delete_to_next_word_end(
2967 &DeleteToNextWordEnd {
2968 ignore_newlines: true,
2969 ignore_brackets: false,
2970 },
2971 window,
2972 cx,
2973 );
2974 });
2975 cx.assert_editor_state(r#"ˇ"#);
2976
2977 cx.update_editor(|editor, window, cx| {
2978 editor.delete_to_next_word_end(
2979 &DeleteToNextWordEnd {
2980 ignore_newlines: true,
2981 ignore_brackets: false,
2982 },
2983 window,
2984 cx,
2985 );
2986 });
2987 cx.assert_editor_state(r#"ˇ"#);
2988
2989 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2990 cx.update_editor(|editor, window, cx| {
2991 editor.delete_to_previous_word_start(
2992 &DeleteToPreviousWordStart {
2993 ignore_newlines: true,
2994 ignore_brackets: true,
2995 },
2996 window,
2997 cx,
2998 );
2999 });
3000 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
3001}
3002
3003#[gpui::test]
3004fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
3005 init_test(cx, |_| {});
3006
3007 let editor = cx.add_window(|window, cx| {
3008 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
3009 build_editor(buffer, window, cx)
3010 });
3011 let del_to_prev_word_start = DeleteToPreviousWordStart {
3012 ignore_newlines: false,
3013 ignore_brackets: false,
3014 };
3015 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
3016 ignore_newlines: true,
3017 ignore_brackets: false,
3018 };
3019
3020 _ = editor.update(cx, |editor, window, cx| {
3021 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3022 s.select_display_ranges([
3023 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
3024 ])
3025 });
3026 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3027 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
3028 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3029 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
3030 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3031 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
3032 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3033 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
3034 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3035 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3036 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3037 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3038 });
3039}
3040
3041#[gpui::test]
3042fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3043 init_test(cx, |_| {});
3044
3045 let editor = cx.add_window(|window, cx| {
3046 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3047 build_editor(buffer, window, cx)
3048 });
3049 let del_to_next_word_end = DeleteToNextWordEnd {
3050 ignore_newlines: false,
3051 ignore_brackets: false,
3052 };
3053 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3054 ignore_newlines: true,
3055 ignore_brackets: false,
3056 };
3057
3058 _ = editor.update(cx, |editor, window, cx| {
3059 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3060 s.select_display_ranges([
3061 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3062 ])
3063 });
3064 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3065 assert_eq!(
3066 editor.buffer.read(cx).read(cx).text(),
3067 "one\n two\nthree\n four"
3068 );
3069 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3070 assert_eq!(
3071 editor.buffer.read(cx).read(cx).text(),
3072 "\n two\nthree\n four"
3073 );
3074 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3075 assert_eq!(
3076 editor.buffer.read(cx).read(cx).text(),
3077 "two\nthree\n four"
3078 );
3079 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3080 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3081 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3082 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3083 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3084 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3085 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3086 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3087 });
3088}
3089
3090#[gpui::test]
3091fn test_newline(cx: &mut TestAppContext) {
3092 init_test(cx, |_| {});
3093
3094 let editor = cx.add_window(|window, cx| {
3095 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3096 build_editor(buffer, window, cx)
3097 });
3098
3099 _ = editor.update(cx, |editor, window, cx| {
3100 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3101 s.select_display_ranges([
3102 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3103 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3104 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3105 ])
3106 });
3107
3108 editor.newline(&Newline, window, cx);
3109 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3110 });
3111}
3112
3113#[gpui::test]
3114fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3115 init_test(cx, |_| {});
3116
3117 let editor = cx.add_window(|window, cx| {
3118 let buffer = MultiBuffer::build_simple(
3119 "
3120 a
3121 b(
3122 X
3123 )
3124 c(
3125 X
3126 )
3127 "
3128 .unindent()
3129 .as_str(),
3130 cx,
3131 );
3132 let mut editor = build_editor(buffer, window, cx);
3133 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3134 s.select_ranges([
3135 Point::new(2, 4)..Point::new(2, 5),
3136 Point::new(5, 4)..Point::new(5, 5),
3137 ])
3138 });
3139 editor
3140 });
3141
3142 _ = editor.update(cx, |editor, window, cx| {
3143 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3144 editor.buffer.update(cx, |buffer, cx| {
3145 buffer.edit(
3146 [
3147 (Point::new(1, 2)..Point::new(3, 0), ""),
3148 (Point::new(4, 2)..Point::new(6, 0), ""),
3149 ],
3150 None,
3151 cx,
3152 );
3153 assert_eq!(
3154 buffer.read(cx).text(),
3155 "
3156 a
3157 b()
3158 c()
3159 "
3160 .unindent()
3161 );
3162 });
3163 assert_eq!(
3164 editor.selections.ranges(cx),
3165 &[
3166 Point::new(1, 2)..Point::new(1, 2),
3167 Point::new(2, 2)..Point::new(2, 2),
3168 ],
3169 );
3170
3171 editor.newline(&Newline, window, cx);
3172 assert_eq!(
3173 editor.text(cx),
3174 "
3175 a
3176 b(
3177 )
3178 c(
3179 )
3180 "
3181 .unindent()
3182 );
3183
3184 // The selections are moved after the inserted newlines
3185 assert_eq!(
3186 editor.selections.ranges(cx),
3187 &[
3188 Point::new(2, 0)..Point::new(2, 0),
3189 Point::new(4, 0)..Point::new(4, 0),
3190 ],
3191 );
3192 });
3193}
3194
3195#[gpui::test]
3196async fn test_newline_above(cx: &mut TestAppContext) {
3197 init_test(cx, |settings| {
3198 settings.defaults.tab_size = NonZeroU32::new(4)
3199 });
3200
3201 let language = Arc::new(
3202 Language::new(
3203 LanguageConfig::default(),
3204 Some(tree_sitter_rust::LANGUAGE.into()),
3205 )
3206 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3207 .unwrap(),
3208 );
3209
3210 let mut cx = EditorTestContext::new(cx).await;
3211 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3212 cx.set_state(indoc! {"
3213 const a: ˇA = (
3214 (ˇ
3215 «const_functionˇ»(ˇ),
3216 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3217 )ˇ
3218 ˇ);ˇ
3219 "});
3220
3221 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3222 cx.assert_editor_state(indoc! {"
3223 ˇ
3224 const a: A = (
3225 ˇ
3226 (
3227 ˇ
3228 ˇ
3229 const_function(),
3230 ˇ
3231 ˇ
3232 ˇ
3233 ˇ
3234 something_else,
3235 ˇ
3236 )
3237 ˇ
3238 ˇ
3239 );
3240 "});
3241}
3242
3243#[gpui::test]
3244async fn test_newline_below(cx: &mut TestAppContext) {
3245 init_test(cx, |settings| {
3246 settings.defaults.tab_size = NonZeroU32::new(4)
3247 });
3248
3249 let language = Arc::new(
3250 Language::new(
3251 LanguageConfig::default(),
3252 Some(tree_sitter_rust::LANGUAGE.into()),
3253 )
3254 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3255 .unwrap(),
3256 );
3257
3258 let mut cx = EditorTestContext::new(cx).await;
3259 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3260 cx.set_state(indoc! {"
3261 const a: ˇA = (
3262 (ˇ
3263 «const_functionˇ»(ˇ),
3264 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3265 )ˇ
3266 ˇ);ˇ
3267 "});
3268
3269 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3270 cx.assert_editor_state(indoc! {"
3271 const a: A = (
3272 ˇ
3273 (
3274 ˇ
3275 const_function(),
3276 ˇ
3277 ˇ
3278 something_else,
3279 ˇ
3280 ˇ
3281 ˇ
3282 ˇ
3283 )
3284 ˇ
3285 );
3286 ˇ
3287 ˇ
3288 "});
3289}
3290
3291#[gpui::test]
3292async fn test_newline_comments(cx: &mut TestAppContext) {
3293 init_test(cx, |settings| {
3294 settings.defaults.tab_size = NonZeroU32::new(4)
3295 });
3296
3297 let language = Arc::new(Language::new(
3298 LanguageConfig {
3299 line_comments: vec!["// ".into()],
3300 ..LanguageConfig::default()
3301 },
3302 None,
3303 ));
3304 {
3305 let mut cx = EditorTestContext::new(cx).await;
3306 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3307 cx.set_state(indoc! {"
3308 // Fooˇ
3309 "});
3310
3311 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3312 cx.assert_editor_state(indoc! {"
3313 // Foo
3314 // ˇ
3315 "});
3316 // Ensure that we add comment prefix when existing line contains space
3317 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3318 cx.assert_editor_state(
3319 indoc! {"
3320 // Foo
3321 //s
3322 // ˇ
3323 "}
3324 .replace("s", " ") // s is used as space placeholder to prevent format on save
3325 .as_str(),
3326 );
3327 // Ensure that we add comment prefix when existing line does not contain space
3328 cx.set_state(indoc! {"
3329 // Foo
3330 //ˇ
3331 "});
3332 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3333 cx.assert_editor_state(indoc! {"
3334 // Foo
3335 //
3336 // ˇ
3337 "});
3338 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3339 cx.set_state(indoc! {"
3340 ˇ// Foo
3341 "});
3342 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3343 cx.assert_editor_state(indoc! {"
3344
3345 ˇ// Foo
3346 "});
3347 }
3348 // Ensure that comment continuations can be disabled.
3349 update_test_language_settings(cx, |settings| {
3350 settings.defaults.extend_comment_on_newline = Some(false);
3351 });
3352 let mut cx = EditorTestContext::new(cx).await;
3353 cx.set_state(indoc! {"
3354 // Fooˇ
3355 "});
3356 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3357 cx.assert_editor_state(indoc! {"
3358 // Foo
3359 ˇ
3360 "});
3361}
3362
3363#[gpui::test]
3364async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3365 init_test(cx, |settings| {
3366 settings.defaults.tab_size = NonZeroU32::new(4)
3367 });
3368
3369 let language = Arc::new(Language::new(
3370 LanguageConfig {
3371 line_comments: vec!["// ".into(), "/// ".into()],
3372 ..LanguageConfig::default()
3373 },
3374 None,
3375 ));
3376 {
3377 let mut cx = EditorTestContext::new(cx).await;
3378 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3379 cx.set_state(indoc! {"
3380 //ˇ
3381 "});
3382 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3383 cx.assert_editor_state(indoc! {"
3384 //
3385 // ˇ
3386 "});
3387
3388 cx.set_state(indoc! {"
3389 ///ˇ
3390 "});
3391 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3392 cx.assert_editor_state(indoc! {"
3393 ///
3394 /// ˇ
3395 "});
3396 }
3397}
3398
3399#[gpui::test]
3400async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3401 init_test(cx, |settings| {
3402 settings.defaults.tab_size = NonZeroU32::new(4)
3403 });
3404
3405 let language = Arc::new(
3406 Language::new(
3407 LanguageConfig {
3408 documentation_comment: Some(language::BlockCommentConfig {
3409 start: "/**".into(),
3410 end: "*/".into(),
3411 prefix: "* ".into(),
3412 tab_size: 1,
3413 }),
3414
3415 ..LanguageConfig::default()
3416 },
3417 Some(tree_sitter_rust::LANGUAGE.into()),
3418 )
3419 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3420 .unwrap(),
3421 );
3422
3423 {
3424 let mut cx = EditorTestContext::new(cx).await;
3425 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3426 cx.set_state(indoc! {"
3427 /**ˇ
3428 "});
3429
3430 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3431 cx.assert_editor_state(indoc! {"
3432 /**
3433 * ˇ
3434 "});
3435 // Ensure that if cursor is before the comment start,
3436 // we do not actually insert a comment prefix.
3437 cx.set_state(indoc! {"
3438 ˇ/**
3439 "});
3440 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3441 cx.assert_editor_state(indoc! {"
3442
3443 ˇ/**
3444 "});
3445 // Ensure that if cursor is between it doesn't add comment prefix.
3446 cx.set_state(indoc! {"
3447 /*ˇ*
3448 "});
3449 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3450 cx.assert_editor_state(indoc! {"
3451 /*
3452 ˇ*
3453 "});
3454 // Ensure that if suffix exists on same line after cursor it adds new line.
3455 cx.set_state(indoc! {"
3456 /**ˇ*/
3457 "});
3458 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3459 cx.assert_editor_state(indoc! {"
3460 /**
3461 * ˇ
3462 */
3463 "});
3464 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3465 cx.set_state(indoc! {"
3466 /**ˇ */
3467 "});
3468 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3469 cx.assert_editor_state(indoc! {"
3470 /**
3471 * ˇ
3472 */
3473 "});
3474 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3475 cx.set_state(indoc! {"
3476 /** ˇ*/
3477 "});
3478 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3479 cx.assert_editor_state(
3480 indoc! {"
3481 /**s
3482 * ˇ
3483 */
3484 "}
3485 .replace("s", " ") // s is used as space placeholder to prevent format on save
3486 .as_str(),
3487 );
3488 // Ensure that delimiter space is preserved when newline on already
3489 // spaced delimiter.
3490 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3491 cx.assert_editor_state(
3492 indoc! {"
3493 /**s
3494 *s
3495 * ˇ
3496 */
3497 "}
3498 .replace("s", " ") // s is used as space placeholder to prevent format on save
3499 .as_str(),
3500 );
3501 // Ensure that delimiter space is preserved when space is not
3502 // on existing delimiter.
3503 cx.set_state(indoc! {"
3504 /**
3505 *ˇ
3506 */
3507 "});
3508 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3509 cx.assert_editor_state(indoc! {"
3510 /**
3511 *
3512 * ˇ
3513 */
3514 "});
3515 // Ensure that if suffix exists on same line after cursor it
3516 // doesn't add extra new line if prefix is not on same line.
3517 cx.set_state(indoc! {"
3518 /**
3519 ˇ*/
3520 "});
3521 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3522 cx.assert_editor_state(indoc! {"
3523 /**
3524
3525 ˇ*/
3526 "});
3527 // Ensure that it detects suffix after existing prefix.
3528 cx.set_state(indoc! {"
3529 /**ˇ/
3530 "});
3531 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3532 cx.assert_editor_state(indoc! {"
3533 /**
3534 ˇ/
3535 "});
3536 // Ensure that if suffix exists on same line before
3537 // cursor it does not add comment prefix.
3538 cx.set_state(indoc! {"
3539 /** */ˇ
3540 "});
3541 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3542 cx.assert_editor_state(indoc! {"
3543 /** */
3544 ˇ
3545 "});
3546 // Ensure that if suffix exists on same line before
3547 // cursor it does not add comment prefix.
3548 cx.set_state(indoc! {"
3549 /**
3550 *
3551 */ˇ
3552 "});
3553 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3554 cx.assert_editor_state(indoc! {"
3555 /**
3556 *
3557 */
3558 ˇ
3559 "});
3560
3561 // Ensure that inline comment followed by code
3562 // doesn't add comment prefix on newline
3563 cx.set_state(indoc! {"
3564 /** */ textˇ
3565 "});
3566 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3567 cx.assert_editor_state(indoc! {"
3568 /** */ text
3569 ˇ
3570 "});
3571
3572 // Ensure that text after comment end tag
3573 // doesn't add comment prefix on newline
3574 cx.set_state(indoc! {"
3575 /**
3576 *
3577 */ˇtext
3578 "});
3579 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3580 cx.assert_editor_state(indoc! {"
3581 /**
3582 *
3583 */
3584 ˇtext
3585 "});
3586
3587 // Ensure if not comment block it doesn't
3588 // add comment prefix on newline
3589 cx.set_state(indoc! {"
3590 * textˇ
3591 "});
3592 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3593 cx.assert_editor_state(indoc! {"
3594 * text
3595 ˇ
3596 "});
3597 }
3598 // Ensure that comment continuations can be disabled.
3599 update_test_language_settings(cx, |settings| {
3600 settings.defaults.extend_comment_on_newline = Some(false);
3601 });
3602 let mut cx = EditorTestContext::new(cx).await;
3603 cx.set_state(indoc! {"
3604 /**ˇ
3605 "});
3606 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3607 cx.assert_editor_state(indoc! {"
3608 /**
3609 ˇ
3610 "});
3611}
3612
3613#[gpui::test]
3614async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3615 init_test(cx, |settings| {
3616 settings.defaults.tab_size = NonZeroU32::new(4)
3617 });
3618
3619 let lua_language = Arc::new(Language::new(
3620 LanguageConfig {
3621 line_comments: vec!["--".into()],
3622 block_comment: Some(language::BlockCommentConfig {
3623 start: "--[[".into(),
3624 prefix: "".into(),
3625 end: "]]".into(),
3626 tab_size: 0,
3627 }),
3628 ..LanguageConfig::default()
3629 },
3630 None,
3631 ));
3632
3633 let mut cx = EditorTestContext::new(cx).await;
3634 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3635
3636 // Line with line comment should extend
3637 cx.set_state(indoc! {"
3638 --ˇ
3639 "});
3640 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3641 cx.assert_editor_state(indoc! {"
3642 --
3643 --ˇ
3644 "});
3645
3646 // Line with block comment that matches line comment should not extend
3647 cx.set_state(indoc! {"
3648 --[[ˇ
3649 "});
3650 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3651 cx.assert_editor_state(indoc! {"
3652 --[[
3653 ˇ
3654 "});
3655}
3656
3657#[gpui::test]
3658fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3659 init_test(cx, |_| {});
3660
3661 let editor = cx.add_window(|window, cx| {
3662 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3663 let mut editor = build_editor(buffer, window, cx);
3664 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3665 s.select_ranges([3..4, 11..12, 19..20])
3666 });
3667 editor
3668 });
3669
3670 _ = editor.update(cx, |editor, window, cx| {
3671 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3672 editor.buffer.update(cx, |buffer, cx| {
3673 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3674 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3675 });
3676 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3677
3678 editor.insert("Z", window, cx);
3679 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3680
3681 // The selections are moved after the inserted characters
3682 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3683 });
3684}
3685
3686#[gpui::test]
3687async fn test_tab(cx: &mut TestAppContext) {
3688 init_test(cx, |settings| {
3689 settings.defaults.tab_size = NonZeroU32::new(3)
3690 });
3691
3692 let mut cx = EditorTestContext::new(cx).await;
3693 cx.set_state(indoc! {"
3694 ˇabˇc
3695 ˇ🏀ˇ🏀ˇefg
3696 dˇ
3697 "});
3698 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3699 cx.assert_editor_state(indoc! {"
3700 ˇab ˇc
3701 ˇ🏀 ˇ🏀 ˇefg
3702 d ˇ
3703 "});
3704
3705 cx.set_state(indoc! {"
3706 a
3707 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3708 "});
3709 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3710 cx.assert_editor_state(indoc! {"
3711 a
3712 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3713 "});
3714}
3715
3716#[gpui::test]
3717async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3718 init_test(cx, |_| {});
3719
3720 let mut cx = EditorTestContext::new(cx).await;
3721 let language = Arc::new(
3722 Language::new(
3723 LanguageConfig::default(),
3724 Some(tree_sitter_rust::LANGUAGE.into()),
3725 )
3726 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3727 .unwrap(),
3728 );
3729 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3730
3731 // test when all cursors are not at suggested indent
3732 // then simply move to their suggested indent location
3733 cx.set_state(indoc! {"
3734 const a: B = (
3735 c(
3736 ˇ
3737 ˇ )
3738 );
3739 "});
3740 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3741 cx.assert_editor_state(indoc! {"
3742 const a: B = (
3743 c(
3744 ˇ
3745 ˇ)
3746 );
3747 "});
3748
3749 // test cursor already at suggested indent not moving when
3750 // other cursors are yet to reach their suggested indents
3751 cx.set_state(indoc! {"
3752 ˇ
3753 const a: B = (
3754 c(
3755 d(
3756 ˇ
3757 )
3758 ˇ
3759 ˇ )
3760 );
3761 "});
3762 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3763 cx.assert_editor_state(indoc! {"
3764 ˇ
3765 const a: B = (
3766 c(
3767 d(
3768 ˇ
3769 )
3770 ˇ
3771 ˇ)
3772 );
3773 "});
3774 // test when all cursors are at suggested indent then tab is inserted
3775 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3776 cx.assert_editor_state(indoc! {"
3777 ˇ
3778 const a: B = (
3779 c(
3780 d(
3781 ˇ
3782 )
3783 ˇ
3784 ˇ)
3785 );
3786 "});
3787
3788 // test when current indent is less than suggested indent,
3789 // we adjust line to match suggested indent and move cursor to it
3790 //
3791 // when no other cursor is at word boundary, all of them should move
3792 cx.set_state(indoc! {"
3793 const a: B = (
3794 c(
3795 d(
3796 ˇ
3797 ˇ )
3798 ˇ )
3799 );
3800 "});
3801 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3802 cx.assert_editor_state(indoc! {"
3803 const a: B = (
3804 c(
3805 d(
3806 ˇ
3807 ˇ)
3808 ˇ)
3809 );
3810 "});
3811
3812 // test when current indent is less than suggested indent,
3813 // we adjust line to match suggested indent and move cursor to it
3814 //
3815 // when some other cursor is at word boundary, it should not move
3816 cx.set_state(indoc! {"
3817 const a: B = (
3818 c(
3819 d(
3820 ˇ
3821 ˇ )
3822 ˇ)
3823 );
3824 "});
3825 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3826 cx.assert_editor_state(indoc! {"
3827 const a: B = (
3828 c(
3829 d(
3830 ˇ
3831 ˇ)
3832 ˇ)
3833 );
3834 "});
3835
3836 // test when current indent is more than suggested indent,
3837 // we just move cursor to current indent instead of suggested indent
3838 //
3839 // when no other cursor is at word boundary, all of them should move
3840 cx.set_state(indoc! {"
3841 const a: B = (
3842 c(
3843 d(
3844 ˇ
3845 ˇ )
3846 ˇ )
3847 );
3848 "});
3849 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3850 cx.assert_editor_state(indoc! {"
3851 const a: B = (
3852 c(
3853 d(
3854 ˇ
3855 ˇ)
3856 ˇ)
3857 );
3858 "});
3859 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3860 cx.assert_editor_state(indoc! {"
3861 const a: B = (
3862 c(
3863 d(
3864 ˇ
3865 ˇ)
3866 ˇ)
3867 );
3868 "});
3869
3870 // test when current indent is more than suggested indent,
3871 // we just move cursor to current indent instead of suggested indent
3872 //
3873 // when some other cursor is at word boundary, it doesn't move
3874 cx.set_state(indoc! {"
3875 const a: B = (
3876 c(
3877 d(
3878 ˇ
3879 ˇ )
3880 ˇ)
3881 );
3882 "});
3883 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3884 cx.assert_editor_state(indoc! {"
3885 const a: B = (
3886 c(
3887 d(
3888 ˇ
3889 ˇ)
3890 ˇ)
3891 );
3892 "});
3893
3894 // handle auto-indent when there are multiple cursors on the same line
3895 cx.set_state(indoc! {"
3896 const a: B = (
3897 c(
3898 ˇ ˇ
3899 ˇ )
3900 );
3901 "});
3902 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3903 cx.assert_editor_state(indoc! {"
3904 const a: B = (
3905 c(
3906 ˇ
3907 ˇ)
3908 );
3909 "});
3910}
3911
3912#[gpui::test]
3913async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3914 init_test(cx, |settings| {
3915 settings.defaults.tab_size = NonZeroU32::new(3)
3916 });
3917
3918 let mut cx = EditorTestContext::new(cx).await;
3919 cx.set_state(indoc! {"
3920 ˇ
3921 \t ˇ
3922 \t ˇ
3923 \t ˇ
3924 \t \t\t \t \t\t \t\t \t \t ˇ
3925 "});
3926
3927 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3928 cx.assert_editor_state(indoc! {"
3929 ˇ
3930 \t ˇ
3931 \t ˇ
3932 \t ˇ
3933 \t \t\t \t \t\t \t\t \t \t ˇ
3934 "});
3935}
3936
3937#[gpui::test]
3938async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3939 init_test(cx, |settings| {
3940 settings.defaults.tab_size = NonZeroU32::new(4)
3941 });
3942
3943 let language = Arc::new(
3944 Language::new(
3945 LanguageConfig::default(),
3946 Some(tree_sitter_rust::LANGUAGE.into()),
3947 )
3948 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3949 .unwrap(),
3950 );
3951
3952 let mut cx = EditorTestContext::new(cx).await;
3953 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3954 cx.set_state(indoc! {"
3955 fn a() {
3956 if b {
3957 \t ˇc
3958 }
3959 }
3960 "});
3961
3962 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3963 cx.assert_editor_state(indoc! {"
3964 fn a() {
3965 if b {
3966 ˇc
3967 }
3968 }
3969 "});
3970}
3971
3972#[gpui::test]
3973async fn test_indent_outdent(cx: &mut TestAppContext) {
3974 init_test(cx, |settings| {
3975 settings.defaults.tab_size = NonZeroU32::new(4);
3976 });
3977
3978 let mut cx = EditorTestContext::new(cx).await;
3979
3980 cx.set_state(indoc! {"
3981 «oneˇ» «twoˇ»
3982 three
3983 four
3984 "});
3985 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3986 cx.assert_editor_state(indoc! {"
3987 «oneˇ» «twoˇ»
3988 three
3989 four
3990 "});
3991
3992 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3993 cx.assert_editor_state(indoc! {"
3994 «oneˇ» «twoˇ»
3995 three
3996 four
3997 "});
3998
3999 // select across line ending
4000 cx.set_state(indoc! {"
4001 one two
4002 t«hree
4003 ˇ» four
4004 "});
4005 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4006 cx.assert_editor_state(indoc! {"
4007 one two
4008 t«hree
4009 ˇ» four
4010 "});
4011
4012 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4013 cx.assert_editor_state(indoc! {"
4014 one two
4015 t«hree
4016 ˇ» four
4017 "});
4018
4019 // Ensure that indenting/outdenting works when the cursor is at column 0.
4020 cx.set_state(indoc! {"
4021 one two
4022 ˇthree
4023 four
4024 "});
4025 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4026 cx.assert_editor_state(indoc! {"
4027 one two
4028 ˇthree
4029 four
4030 "});
4031
4032 cx.set_state(indoc! {"
4033 one two
4034 ˇ three
4035 four
4036 "});
4037 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4038 cx.assert_editor_state(indoc! {"
4039 one two
4040 ˇthree
4041 four
4042 "});
4043}
4044
4045#[gpui::test]
4046async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4047 // This is a regression test for issue #33761
4048 init_test(cx, |_| {});
4049
4050 let mut cx = EditorTestContext::new(cx).await;
4051 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4052 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4053
4054 cx.set_state(
4055 r#"ˇ# ingress:
4056ˇ# api:
4057ˇ# enabled: false
4058ˇ# pathType: Prefix
4059ˇ# console:
4060ˇ# enabled: false
4061ˇ# pathType: Prefix
4062"#,
4063 );
4064
4065 // Press tab to indent all lines
4066 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4067
4068 cx.assert_editor_state(
4069 r#" ˇ# ingress:
4070 ˇ# api:
4071 ˇ# enabled: false
4072 ˇ# pathType: Prefix
4073 ˇ# console:
4074 ˇ# enabled: false
4075 ˇ# pathType: Prefix
4076"#,
4077 );
4078}
4079
4080#[gpui::test]
4081async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4082 // This is a test to make sure our fix for issue #33761 didn't break anything
4083 init_test(cx, |_| {});
4084
4085 let mut cx = EditorTestContext::new(cx).await;
4086 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4087 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4088
4089 cx.set_state(
4090 r#"ˇingress:
4091ˇ api:
4092ˇ enabled: false
4093ˇ pathType: Prefix
4094"#,
4095 );
4096
4097 // Press tab to indent all lines
4098 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4099
4100 cx.assert_editor_state(
4101 r#"ˇingress:
4102 ˇapi:
4103 ˇenabled: false
4104 ˇpathType: Prefix
4105"#,
4106 );
4107}
4108
4109#[gpui::test]
4110async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4111 init_test(cx, |settings| {
4112 settings.defaults.hard_tabs = Some(true);
4113 });
4114
4115 let mut cx = EditorTestContext::new(cx).await;
4116
4117 // select two ranges on one line
4118 cx.set_state(indoc! {"
4119 «oneˇ» «twoˇ»
4120 three
4121 four
4122 "});
4123 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4124 cx.assert_editor_state(indoc! {"
4125 \t«oneˇ» «twoˇ»
4126 three
4127 four
4128 "});
4129 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4130 cx.assert_editor_state(indoc! {"
4131 \t\t«oneˇ» «twoˇ»
4132 three
4133 four
4134 "});
4135 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4136 cx.assert_editor_state(indoc! {"
4137 \t«oneˇ» «twoˇ»
4138 three
4139 four
4140 "});
4141 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4142 cx.assert_editor_state(indoc! {"
4143 «oneˇ» «twoˇ»
4144 three
4145 four
4146 "});
4147
4148 // select across a line ending
4149 cx.set_state(indoc! {"
4150 one two
4151 t«hree
4152 ˇ»four
4153 "});
4154 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4155 cx.assert_editor_state(indoc! {"
4156 one two
4157 \tt«hree
4158 ˇ»four
4159 "});
4160 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4161 cx.assert_editor_state(indoc! {"
4162 one two
4163 \t\tt«hree
4164 ˇ»four
4165 "});
4166 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4167 cx.assert_editor_state(indoc! {"
4168 one two
4169 \tt«hree
4170 ˇ»four
4171 "});
4172 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4173 cx.assert_editor_state(indoc! {"
4174 one two
4175 t«hree
4176 ˇ»four
4177 "});
4178
4179 // Ensure that indenting/outdenting works when the cursor is at column 0.
4180 cx.set_state(indoc! {"
4181 one two
4182 ˇthree
4183 four
4184 "});
4185 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4186 cx.assert_editor_state(indoc! {"
4187 one two
4188 ˇthree
4189 four
4190 "});
4191 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4192 cx.assert_editor_state(indoc! {"
4193 one two
4194 \tˇthree
4195 four
4196 "});
4197 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4198 cx.assert_editor_state(indoc! {"
4199 one two
4200 ˇthree
4201 four
4202 "});
4203}
4204
4205#[gpui::test]
4206fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4207 init_test(cx, |settings| {
4208 settings.languages.0.extend([
4209 (
4210 "TOML".into(),
4211 LanguageSettingsContent {
4212 tab_size: NonZeroU32::new(2),
4213 ..Default::default()
4214 },
4215 ),
4216 (
4217 "Rust".into(),
4218 LanguageSettingsContent {
4219 tab_size: NonZeroU32::new(4),
4220 ..Default::default()
4221 },
4222 ),
4223 ]);
4224 });
4225
4226 let toml_language = Arc::new(Language::new(
4227 LanguageConfig {
4228 name: "TOML".into(),
4229 ..Default::default()
4230 },
4231 None,
4232 ));
4233 let rust_language = Arc::new(Language::new(
4234 LanguageConfig {
4235 name: "Rust".into(),
4236 ..Default::default()
4237 },
4238 None,
4239 ));
4240
4241 let toml_buffer =
4242 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4243 let rust_buffer =
4244 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4245 let multibuffer = cx.new(|cx| {
4246 let mut multibuffer = MultiBuffer::new(ReadWrite);
4247 multibuffer.push_excerpts(
4248 toml_buffer.clone(),
4249 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4250 cx,
4251 );
4252 multibuffer.push_excerpts(
4253 rust_buffer.clone(),
4254 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4255 cx,
4256 );
4257 multibuffer
4258 });
4259
4260 cx.add_window(|window, cx| {
4261 let mut editor = build_editor(multibuffer, window, cx);
4262
4263 assert_eq!(
4264 editor.text(cx),
4265 indoc! {"
4266 a = 1
4267 b = 2
4268
4269 const c: usize = 3;
4270 "}
4271 );
4272
4273 select_ranges(
4274 &mut editor,
4275 indoc! {"
4276 «aˇ» = 1
4277 b = 2
4278
4279 «const c:ˇ» usize = 3;
4280 "},
4281 window,
4282 cx,
4283 );
4284
4285 editor.tab(&Tab, window, cx);
4286 assert_text_with_selections(
4287 &mut editor,
4288 indoc! {"
4289 «aˇ» = 1
4290 b = 2
4291
4292 «const c:ˇ» usize = 3;
4293 "},
4294 cx,
4295 );
4296 editor.backtab(&Backtab, window, cx);
4297 assert_text_with_selections(
4298 &mut editor,
4299 indoc! {"
4300 «aˇ» = 1
4301 b = 2
4302
4303 «const c:ˇ» usize = 3;
4304 "},
4305 cx,
4306 );
4307
4308 editor
4309 });
4310}
4311
4312#[gpui::test]
4313async fn test_backspace(cx: &mut TestAppContext) {
4314 init_test(cx, |_| {});
4315
4316 let mut cx = EditorTestContext::new(cx).await;
4317
4318 // Basic backspace
4319 cx.set_state(indoc! {"
4320 onˇe two three
4321 fou«rˇ» five six
4322 seven «ˇeight nine
4323 »ten
4324 "});
4325 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4326 cx.assert_editor_state(indoc! {"
4327 oˇe two three
4328 fouˇ five six
4329 seven ˇten
4330 "});
4331
4332 // Test backspace inside and around indents
4333 cx.set_state(indoc! {"
4334 zero
4335 ˇone
4336 ˇtwo
4337 ˇ ˇ ˇ three
4338 ˇ ˇ four
4339 "});
4340 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4341 cx.assert_editor_state(indoc! {"
4342 zero
4343 ˇone
4344 ˇtwo
4345 ˇ threeˇ four
4346 "});
4347}
4348
4349#[gpui::test]
4350async fn test_delete(cx: &mut TestAppContext) {
4351 init_test(cx, |_| {});
4352
4353 let mut cx = EditorTestContext::new(cx).await;
4354 cx.set_state(indoc! {"
4355 onˇe two three
4356 fou«rˇ» five six
4357 seven «ˇeight nine
4358 »ten
4359 "});
4360 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4361 cx.assert_editor_state(indoc! {"
4362 onˇ two three
4363 fouˇ five six
4364 seven ˇten
4365 "});
4366}
4367
4368#[gpui::test]
4369fn test_delete_line(cx: &mut TestAppContext) {
4370 init_test(cx, |_| {});
4371
4372 let editor = cx.add_window(|window, cx| {
4373 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4374 build_editor(buffer, window, cx)
4375 });
4376 _ = editor.update(cx, |editor, window, cx| {
4377 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4378 s.select_display_ranges([
4379 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4380 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4381 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4382 ])
4383 });
4384 editor.delete_line(&DeleteLine, window, cx);
4385 assert_eq!(editor.display_text(cx), "ghi");
4386 assert_eq!(
4387 editor.selections.display_ranges(cx),
4388 vec![
4389 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4390 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4391 ]
4392 );
4393 });
4394
4395 let editor = cx.add_window(|window, cx| {
4396 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4397 build_editor(buffer, window, cx)
4398 });
4399 _ = editor.update(cx, |editor, window, cx| {
4400 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4401 s.select_display_ranges([
4402 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4403 ])
4404 });
4405 editor.delete_line(&DeleteLine, window, cx);
4406 assert_eq!(editor.display_text(cx), "ghi\n");
4407 assert_eq!(
4408 editor.selections.display_ranges(cx),
4409 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4410 );
4411 });
4412
4413 let editor = cx.add_window(|window, cx| {
4414 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4415 build_editor(buffer, window, cx)
4416 });
4417 _ = editor.update(cx, |editor, window, cx| {
4418 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4419 s.select_display_ranges([
4420 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4421 ])
4422 });
4423 editor.delete_line(&DeleteLine, window, cx);
4424 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4425 assert_eq!(
4426 editor.selections.display_ranges(cx),
4427 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4428 );
4429 });
4430}
4431
4432#[gpui::test]
4433fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4434 init_test(cx, |_| {});
4435
4436 cx.add_window(|window, cx| {
4437 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4438 let mut editor = build_editor(buffer.clone(), window, cx);
4439 let buffer = buffer.read(cx).as_singleton().unwrap();
4440
4441 assert_eq!(
4442 editor.selections.ranges::<Point>(cx),
4443 &[Point::new(0, 0)..Point::new(0, 0)]
4444 );
4445
4446 // When on single line, replace newline at end by space
4447 editor.join_lines(&JoinLines, window, cx);
4448 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4449 assert_eq!(
4450 editor.selections.ranges::<Point>(cx),
4451 &[Point::new(0, 3)..Point::new(0, 3)]
4452 );
4453
4454 // When multiple lines are selected, remove newlines that are spanned by the selection
4455 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4456 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4457 });
4458 editor.join_lines(&JoinLines, window, cx);
4459 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4460 assert_eq!(
4461 editor.selections.ranges::<Point>(cx),
4462 &[Point::new(0, 11)..Point::new(0, 11)]
4463 );
4464
4465 // Undo should be transactional
4466 editor.undo(&Undo, window, cx);
4467 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4468 assert_eq!(
4469 editor.selections.ranges::<Point>(cx),
4470 &[Point::new(0, 5)..Point::new(2, 2)]
4471 );
4472
4473 // When joining an empty line don't insert a space
4474 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4475 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4476 });
4477 editor.join_lines(&JoinLines, window, cx);
4478 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4479 assert_eq!(
4480 editor.selections.ranges::<Point>(cx),
4481 [Point::new(2, 3)..Point::new(2, 3)]
4482 );
4483
4484 // We can remove trailing newlines
4485 editor.join_lines(&JoinLines, window, cx);
4486 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4487 assert_eq!(
4488 editor.selections.ranges::<Point>(cx),
4489 [Point::new(2, 3)..Point::new(2, 3)]
4490 );
4491
4492 // We don't blow up on the last line
4493 editor.join_lines(&JoinLines, window, cx);
4494 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4495 assert_eq!(
4496 editor.selections.ranges::<Point>(cx),
4497 [Point::new(2, 3)..Point::new(2, 3)]
4498 );
4499
4500 // reset to test indentation
4501 editor.buffer.update(cx, |buffer, cx| {
4502 buffer.edit(
4503 [
4504 (Point::new(1, 0)..Point::new(1, 2), " "),
4505 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4506 ],
4507 None,
4508 cx,
4509 )
4510 });
4511
4512 // We remove any leading spaces
4513 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4514 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4515 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4516 });
4517 editor.join_lines(&JoinLines, window, cx);
4518 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4519
4520 // We don't insert a space for a line containing only spaces
4521 editor.join_lines(&JoinLines, window, cx);
4522 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4523
4524 // We ignore any leading tabs
4525 editor.join_lines(&JoinLines, window, cx);
4526 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4527
4528 editor
4529 });
4530}
4531
4532#[gpui::test]
4533fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4534 init_test(cx, |_| {});
4535
4536 cx.add_window(|window, cx| {
4537 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4538 let mut editor = build_editor(buffer.clone(), window, cx);
4539 let buffer = buffer.read(cx).as_singleton().unwrap();
4540
4541 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4542 s.select_ranges([
4543 Point::new(0, 2)..Point::new(1, 1),
4544 Point::new(1, 2)..Point::new(1, 2),
4545 Point::new(3, 1)..Point::new(3, 2),
4546 ])
4547 });
4548
4549 editor.join_lines(&JoinLines, window, cx);
4550 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4551
4552 assert_eq!(
4553 editor.selections.ranges::<Point>(cx),
4554 [
4555 Point::new(0, 7)..Point::new(0, 7),
4556 Point::new(1, 3)..Point::new(1, 3)
4557 ]
4558 );
4559 editor
4560 });
4561}
4562
4563#[gpui::test]
4564async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4565 init_test(cx, |_| {});
4566
4567 let mut cx = EditorTestContext::new(cx).await;
4568
4569 let diff_base = r#"
4570 Line 0
4571 Line 1
4572 Line 2
4573 Line 3
4574 "#
4575 .unindent();
4576
4577 cx.set_state(
4578 &r#"
4579 ˇLine 0
4580 Line 1
4581 Line 2
4582 Line 3
4583 "#
4584 .unindent(),
4585 );
4586
4587 cx.set_head_text(&diff_base);
4588 executor.run_until_parked();
4589
4590 // Join lines
4591 cx.update_editor(|editor, window, cx| {
4592 editor.join_lines(&JoinLines, window, cx);
4593 });
4594 executor.run_until_parked();
4595
4596 cx.assert_editor_state(
4597 &r#"
4598 Line 0ˇ Line 1
4599 Line 2
4600 Line 3
4601 "#
4602 .unindent(),
4603 );
4604 // Join again
4605 cx.update_editor(|editor, window, cx| {
4606 editor.join_lines(&JoinLines, window, cx);
4607 });
4608 executor.run_until_parked();
4609
4610 cx.assert_editor_state(
4611 &r#"
4612 Line 0 Line 1ˇ Line 2
4613 Line 3
4614 "#
4615 .unindent(),
4616 );
4617}
4618
4619#[gpui::test]
4620async fn test_custom_newlines_cause_no_false_positive_diffs(
4621 executor: BackgroundExecutor,
4622 cx: &mut TestAppContext,
4623) {
4624 init_test(cx, |_| {});
4625 let mut cx = EditorTestContext::new(cx).await;
4626 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4627 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4628 executor.run_until_parked();
4629
4630 cx.update_editor(|editor, window, cx| {
4631 let snapshot = editor.snapshot(window, cx);
4632 assert_eq!(
4633 snapshot
4634 .buffer_snapshot()
4635 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
4636 .collect::<Vec<_>>(),
4637 Vec::new(),
4638 "Should not have any diffs for files with custom newlines"
4639 );
4640 });
4641}
4642
4643#[gpui::test]
4644async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4645 init_test(cx, |_| {});
4646
4647 let mut cx = EditorTestContext::new(cx).await;
4648
4649 // Test sort_lines_case_insensitive()
4650 cx.set_state(indoc! {"
4651 «z
4652 y
4653 x
4654 Z
4655 Y
4656 Xˇ»
4657 "});
4658 cx.update_editor(|e, window, cx| {
4659 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4660 });
4661 cx.assert_editor_state(indoc! {"
4662 «x
4663 X
4664 y
4665 Y
4666 z
4667 Zˇ»
4668 "});
4669
4670 // Test sort_lines_by_length()
4671 //
4672 // Demonstrates:
4673 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4674 // - sort is stable
4675 cx.set_state(indoc! {"
4676 «123
4677 æ
4678 12
4679 ∞
4680 1
4681 æˇ»
4682 "});
4683 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4684 cx.assert_editor_state(indoc! {"
4685 «æ
4686 ∞
4687 1
4688 æ
4689 12
4690 123ˇ»
4691 "});
4692
4693 // Test reverse_lines()
4694 cx.set_state(indoc! {"
4695 «5
4696 4
4697 3
4698 2
4699 1ˇ»
4700 "});
4701 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4702 cx.assert_editor_state(indoc! {"
4703 «1
4704 2
4705 3
4706 4
4707 5ˇ»
4708 "});
4709
4710 // Skip testing shuffle_line()
4711
4712 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4713 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4714
4715 // Don't manipulate when cursor is on single line, but expand the selection
4716 cx.set_state(indoc! {"
4717 ddˇdd
4718 ccc
4719 bb
4720 a
4721 "});
4722 cx.update_editor(|e, window, cx| {
4723 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4724 });
4725 cx.assert_editor_state(indoc! {"
4726 «ddddˇ»
4727 ccc
4728 bb
4729 a
4730 "});
4731
4732 // Basic manipulate case
4733 // Start selection moves to column 0
4734 // End of selection shrinks to fit shorter line
4735 cx.set_state(indoc! {"
4736 dd«d
4737 ccc
4738 bb
4739 aaaaaˇ»
4740 "});
4741 cx.update_editor(|e, window, cx| {
4742 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4743 });
4744 cx.assert_editor_state(indoc! {"
4745 «aaaaa
4746 bb
4747 ccc
4748 dddˇ»
4749 "});
4750
4751 // Manipulate case with newlines
4752 cx.set_state(indoc! {"
4753 dd«d
4754 ccc
4755
4756 bb
4757 aaaaa
4758
4759 ˇ»
4760 "});
4761 cx.update_editor(|e, window, cx| {
4762 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4763 });
4764 cx.assert_editor_state(indoc! {"
4765 «
4766
4767 aaaaa
4768 bb
4769 ccc
4770 dddˇ»
4771
4772 "});
4773
4774 // Adding new line
4775 cx.set_state(indoc! {"
4776 aa«a
4777 bbˇ»b
4778 "});
4779 cx.update_editor(|e, window, cx| {
4780 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4781 });
4782 cx.assert_editor_state(indoc! {"
4783 «aaa
4784 bbb
4785 added_lineˇ»
4786 "});
4787
4788 // Removing line
4789 cx.set_state(indoc! {"
4790 aa«a
4791 bbbˇ»
4792 "});
4793 cx.update_editor(|e, window, cx| {
4794 e.manipulate_immutable_lines(window, cx, |lines| {
4795 lines.pop();
4796 })
4797 });
4798 cx.assert_editor_state(indoc! {"
4799 «aaaˇ»
4800 "});
4801
4802 // Removing all lines
4803 cx.set_state(indoc! {"
4804 aa«a
4805 bbbˇ»
4806 "});
4807 cx.update_editor(|e, window, cx| {
4808 e.manipulate_immutable_lines(window, cx, |lines| {
4809 lines.drain(..);
4810 })
4811 });
4812 cx.assert_editor_state(indoc! {"
4813 ˇ
4814 "});
4815}
4816
4817#[gpui::test]
4818async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4819 init_test(cx, |_| {});
4820
4821 let mut cx = EditorTestContext::new(cx).await;
4822
4823 // Consider continuous selection as single selection
4824 cx.set_state(indoc! {"
4825 Aaa«aa
4826 cˇ»c«c
4827 bb
4828 aaaˇ»aa
4829 "});
4830 cx.update_editor(|e, window, cx| {
4831 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4832 });
4833 cx.assert_editor_state(indoc! {"
4834 «Aaaaa
4835 ccc
4836 bb
4837 aaaaaˇ»
4838 "});
4839
4840 cx.set_state(indoc! {"
4841 Aaa«aa
4842 cˇ»c«c
4843 bb
4844 aaaˇ»aa
4845 "});
4846 cx.update_editor(|e, window, cx| {
4847 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4848 });
4849 cx.assert_editor_state(indoc! {"
4850 «Aaaaa
4851 ccc
4852 bbˇ»
4853 "});
4854
4855 // Consider non continuous selection as distinct dedup operations
4856 cx.set_state(indoc! {"
4857 «aaaaa
4858 bb
4859 aaaaa
4860 aaaaaˇ»
4861
4862 aaa«aaˇ»
4863 "});
4864 cx.update_editor(|e, window, cx| {
4865 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4866 });
4867 cx.assert_editor_state(indoc! {"
4868 «aaaaa
4869 bbˇ»
4870
4871 «aaaaaˇ»
4872 "});
4873}
4874
4875#[gpui::test]
4876async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4877 init_test(cx, |_| {});
4878
4879 let mut cx = EditorTestContext::new(cx).await;
4880
4881 cx.set_state(indoc! {"
4882 «Aaa
4883 aAa
4884 Aaaˇ»
4885 "});
4886 cx.update_editor(|e, window, cx| {
4887 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4888 });
4889 cx.assert_editor_state(indoc! {"
4890 «Aaa
4891 aAaˇ»
4892 "});
4893
4894 cx.set_state(indoc! {"
4895 «Aaa
4896 aAa
4897 aaAˇ»
4898 "});
4899 cx.update_editor(|e, window, cx| {
4900 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4901 });
4902 cx.assert_editor_state(indoc! {"
4903 «Aaaˇ»
4904 "});
4905}
4906
4907#[gpui::test]
4908async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4909 init_test(cx, |_| {});
4910
4911 let mut cx = EditorTestContext::new(cx).await;
4912
4913 let js_language = Arc::new(Language::new(
4914 LanguageConfig {
4915 name: "JavaScript".into(),
4916 wrap_characters: Some(language::WrapCharactersConfig {
4917 start_prefix: "<".into(),
4918 start_suffix: ">".into(),
4919 end_prefix: "</".into(),
4920 end_suffix: ">".into(),
4921 }),
4922 ..LanguageConfig::default()
4923 },
4924 None,
4925 ));
4926
4927 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4928
4929 cx.set_state(indoc! {"
4930 «testˇ»
4931 "});
4932 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4933 cx.assert_editor_state(indoc! {"
4934 <«ˇ»>test</«ˇ»>
4935 "});
4936
4937 cx.set_state(indoc! {"
4938 «test
4939 testˇ»
4940 "});
4941 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4942 cx.assert_editor_state(indoc! {"
4943 <«ˇ»>test
4944 test</«ˇ»>
4945 "});
4946
4947 cx.set_state(indoc! {"
4948 teˇst
4949 "});
4950 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4951 cx.assert_editor_state(indoc! {"
4952 te<«ˇ»></«ˇ»>st
4953 "});
4954}
4955
4956#[gpui::test]
4957async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
4958 init_test(cx, |_| {});
4959
4960 let mut cx = EditorTestContext::new(cx).await;
4961
4962 let js_language = Arc::new(Language::new(
4963 LanguageConfig {
4964 name: "JavaScript".into(),
4965 wrap_characters: Some(language::WrapCharactersConfig {
4966 start_prefix: "<".into(),
4967 start_suffix: ">".into(),
4968 end_prefix: "</".into(),
4969 end_suffix: ">".into(),
4970 }),
4971 ..LanguageConfig::default()
4972 },
4973 None,
4974 ));
4975
4976 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4977
4978 cx.set_state(indoc! {"
4979 «testˇ»
4980 «testˇ» «testˇ»
4981 «testˇ»
4982 "});
4983 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4984 cx.assert_editor_state(indoc! {"
4985 <«ˇ»>test</«ˇ»>
4986 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
4987 <«ˇ»>test</«ˇ»>
4988 "});
4989
4990 cx.set_state(indoc! {"
4991 «test
4992 testˇ»
4993 «test
4994 testˇ»
4995 "});
4996 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4997 cx.assert_editor_state(indoc! {"
4998 <«ˇ»>test
4999 test</«ˇ»>
5000 <«ˇ»>test
5001 test</«ˇ»>
5002 "});
5003}
5004
5005#[gpui::test]
5006async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5007 init_test(cx, |_| {});
5008
5009 let mut cx = EditorTestContext::new(cx).await;
5010
5011 let plaintext_language = Arc::new(Language::new(
5012 LanguageConfig {
5013 name: "Plain Text".into(),
5014 ..LanguageConfig::default()
5015 },
5016 None,
5017 ));
5018
5019 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5020
5021 cx.set_state(indoc! {"
5022 «testˇ»
5023 "});
5024 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5025 cx.assert_editor_state(indoc! {"
5026 «testˇ»
5027 "});
5028}
5029
5030#[gpui::test]
5031async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5032 init_test(cx, |_| {});
5033
5034 let mut cx = EditorTestContext::new(cx).await;
5035
5036 // Manipulate with multiple selections on a single line
5037 cx.set_state(indoc! {"
5038 dd«dd
5039 cˇ»c«c
5040 bb
5041 aaaˇ»aa
5042 "});
5043 cx.update_editor(|e, window, cx| {
5044 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5045 });
5046 cx.assert_editor_state(indoc! {"
5047 «aaaaa
5048 bb
5049 ccc
5050 ddddˇ»
5051 "});
5052
5053 // Manipulate with multiple disjoin selections
5054 cx.set_state(indoc! {"
5055 5«
5056 4
5057 3
5058 2
5059 1ˇ»
5060
5061 dd«dd
5062 ccc
5063 bb
5064 aaaˇ»aa
5065 "});
5066 cx.update_editor(|e, window, cx| {
5067 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5068 });
5069 cx.assert_editor_state(indoc! {"
5070 «1
5071 2
5072 3
5073 4
5074 5ˇ»
5075
5076 «aaaaa
5077 bb
5078 ccc
5079 ddddˇ»
5080 "});
5081
5082 // Adding lines on each selection
5083 cx.set_state(indoc! {"
5084 2«
5085 1ˇ»
5086
5087 bb«bb
5088 aaaˇ»aa
5089 "});
5090 cx.update_editor(|e, window, cx| {
5091 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5092 });
5093 cx.assert_editor_state(indoc! {"
5094 «2
5095 1
5096 added lineˇ»
5097
5098 «bbbb
5099 aaaaa
5100 added lineˇ»
5101 "});
5102
5103 // Removing lines on each selection
5104 cx.set_state(indoc! {"
5105 2«
5106 1ˇ»
5107
5108 bb«bb
5109 aaaˇ»aa
5110 "});
5111 cx.update_editor(|e, window, cx| {
5112 e.manipulate_immutable_lines(window, cx, |lines| {
5113 lines.pop();
5114 })
5115 });
5116 cx.assert_editor_state(indoc! {"
5117 «2ˇ»
5118
5119 «bbbbˇ»
5120 "});
5121}
5122
5123#[gpui::test]
5124async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5125 init_test(cx, |settings| {
5126 settings.defaults.tab_size = NonZeroU32::new(3)
5127 });
5128
5129 let mut cx = EditorTestContext::new(cx).await;
5130
5131 // MULTI SELECTION
5132 // Ln.1 "«" tests empty lines
5133 // Ln.9 tests just leading whitespace
5134 cx.set_state(indoc! {"
5135 «
5136 abc // No indentationˇ»
5137 «\tabc // 1 tabˇ»
5138 \t\tabc « ˇ» // 2 tabs
5139 \t ab«c // Tab followed by space
5140 \tabc // Space followed by tab (3 spaces should be the result)
5141 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5142 abˇ»ˇc ˇ ˇ // Already space indented«
5143 \t
5144 \tabc\tdef // Only the leading tab is manipulatedˇ»
5145 "});
5146 cx.update_editor(|e, window, cx| {
5147 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5148 });
5149 cx.assert_editor_state(
5150 indoc! {"
5151 «
5152 abc // No indentation
5153 abc // 1 tab
5154 abc // 2 tabs
5155 abc // Tab followed by space
5156 abc // Space followed by tab (3 spaces should be the result)
5157 abc // Mixed indentation (tab conversion depends on the column)
5158 abc // Already space indented
5159 ·
5160 abc\tdef // Only the leading tab is manipulatedˇ»
5161 "}
5162 .replace("·", "")
5163 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5164 );
5165
5166 // Test on just a few lines, the others should remain unchanged
5167 // Only lines (3, 5, 10, 11) should change
5168 cx.set_state(
5169 indoc! {"
5170 ·
5171 abc // No indentation
5172 \tabcˇ // 1 tab
5173 \t\tabc // 2 tabs
5174 \t abcˇ // Tab followed by space
5175 \tabc // Space followed by tab (3 spaces should be the result)
5176 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5177 abc // Already space indented
5178 «\t
5179 \tabc\tdef // Only the leading tab is manipulatedˇ»
5180 "}
5181 .replace("·", "")
5182 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5183 );
5184 cx.update_editor(|e, window, cx| {
5185 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5186 });
5187 cx.assert_editor_state(
5188 indoc! {"
5189 ·
5190 abc // No indentation
5191 « abc // 1 tabˇ»
5192 \t\tabc // 2 tabs
5193 « abc // Tab followed by spaceˇ»
5194 \tabc // Space followed by tab (3 spaces should be the result)
5195 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5196 abc // Already space indented
5197 « ·
5198 abc\tdef // Only the leading tab is manipulatedˇ»
5199 "}
5200 .replace("·", "")
5201 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5202 );
5203
5204 // SINGLE SELECTION
5205 // Ln.1 "«" tests empty lines
5206 // Ln.9 tests just leading whitespace
5207 cx.set_state(indoc! {"
5208 «
5209 abc // No indentation
5210 \tabc // 1 tab
5211 \t\tabc // 2 tabs
5212 \t abc // Tab followed by space
5213 \tabc // Space followed by tab (3 spaces should be the result)
5214 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5215 abc // Already space indented
5216 \t
5217 \tabc\tdef // Only the leading tab is manipulatedˇ»
5218 "});
5219 cx.update_editor(|e, window, cx| {
5220 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5221 });
5222 cx.assert_editor_state(
5223 indoc! {"
5224 «
5225 abc // No indentation
5226 abc // 1 tab
5227 abc // 2 tabs
5228 abc // Tab followed by space
5229 abc // Space followed by tab (3 spaces should be the result)
5230 abc // Mixed indentation (tab conversion depends on the column)
5231 abc // Already space indented
5232 ·
5233 abc\tdef // Only the leading tab is manipulatedˇ»
5234 "}
5235 .replace("·", "")
5236 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5237 );
5238}
5239
5240#[gpui::test]
5241async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5242 init_test(cx, |settings| {
5243 settings.defaults.tab_size = NonZeroU32::new(3)
5244 });
5245
5246 let mut cx = EditorTestContext::new(cx).await;
5247
5248 // MULTI SELECTION
5249 // Ln.1 "«" tests empty lines
5250 // Ln.11 tests just leading whitespace
5251 cx.set_state(indoc! {"
5252 «
5253 abˇ»ˇc // No indentation
5254 abc ˇ ˇ // 1 space (< 3 so dont convert)
5255 abc « // 2 spaces (< 3 so dont convert)
5256 abc // 3 spaces (convert)
5257 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5258 «\tˇ»\t«\tˇ»abc // Already tab indented
5259 «\t abc // Tab followed by space
5260 \tabc // Space followed by tab (should be consumed due to tab)
5261 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5262 \tˇ» «\t
5263 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5264 "});
5265 cx.update_editor(|e, window, cx| {
5266 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5267 });
5268 cx.assert_editor_state(indoc! {"
5269 «
5270 abc // No indentation
5271 abc // 1 space (< 3 so dont convert)
5272 abc // 2 spaces (< 3 so dont convert)
5273 \tabc // 3 spaces (convert)
5274 \t abc // 5 spaces (1 tab + 2 spaces)
5275 \t\t\tabc // Already tab indented
5276 \t abc // Tab followed by space
5277 \tabc // Space followed by tab (should be consumed due to tab)
5278 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5279 \t\t\t
5280 \tabc \t // Only the leading spaces should be convertedˇ»
5281 "});
5282
5283 // Test on just a few lines, the other should remain unchanged
5284 // Only lines (4, 8, 11, 12) should change
5285 cx.set_state(
5286 indoc! {"
5287 ·
5288 abc // No indentation
5289 abc // 1 space (< 3 so dont convert)
5290 abc // 2 spaces (< 3 so dont convert)
5291 « abc // 3 spaces (convert)ˇ»
5292 abc // 5 spaces (1 tab + 2 spaces)
5293 \t\t\tabc // Already tab indented
5294 \t abc // Tab followed by space
5295 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5296 \t\t \tabc // Mixed indentation
5297 \t \t \t \tabc // Mixed indentation
5298 \t \tˇ
5299 « abc \t // Only the leading spaces should be convertedˇ»
5300 "}
5301 .replace("·", "")
5302 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5303 );
5304 cx.update_editor(|e, window, cx| {
5305 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5306 });
5307 cx.assert_editor_state(
5308 indoc! {"
5309 ·
5310 abc // No indentation
5311 abc // 1 space (< 3 so dont convert)
5312 abc // 2 spaces (< 3 so dont convert)
5313 «\tabc // 3 spaces (convert)ˇ»
5314 abc // 5 spaces (1 tab + 2 spaces)
5315 \t\t\tabc // Already tab indented
5316 \t abc // Tab followed by space
5317 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5318 \t\t \tabc // Mixed indentation
5319 \t \t \t \tabc // Mixed indentation
5320 «\t\t\t
5321 \tabc \t // Only the leading spaces should be convertedˇ»
5322 "}
5323 .replace("·", "")
5324 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5325 );
5326
5327 // SINGLE SELECTION
5328 // Ln.1 "«" tests empty lines
5329 // Ln.11 tests just leading whitespace
5330 cx.set_state(indoc! {"
5331 «
5332 abc // No indentation
5333 abc // 1 space (< 3 so dont convert)
5334 abc // 2 spaces (< 3 so dont convert)
5335 abc // 3 spaces (convert)
5336 abc // 5 spaces (1 tab + 2 spaces)
5337 \t\t\tabc // Already tab indented
5338 \t abc // Tab followed by space
5339 \tabc // Space followed by tab (should be consumed due to tab)
5340 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5341 \t \t
5342 abc \t // Only the leading spaces should be convertedˇ»
5343 "});
5344 cx.update_editor(|e, window, cx| {
5345 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5346 });
5347 cx.assert_editor_state(indoc! {"
5348 «
5349 abc // No indentation
5350 abc // 1 space (< 3 so dont convert)
5351 abc // 2 spaces (< 3 so dont convert)
5352 \tabc // 3 spaces (convert)
5353 \t abc // 5 spaces (1 tab + 2 spaces)
5354 \t\t\tabc // Already tab indented
5355 \t abc // Tab followed by space
5356 \tabc // Space followed by tab (should be consumed due to tab)
5357 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5358 \t\t\t
5359 \tabc \t // Only the leading spaces should be convertedˇ»
5360 "});
5361}
5362
5363#[gpui::test]
5364async fn test_toggle_case(cx: &mut TestAppContext) {
5365 init_test(cx, |_| {});
5366
5367 let mut cx = EditorTestContext::new(cx).await;
5368
5369 // If all lower case -> upper case
5370 cx.set_state(indoc! {"
5371 «hello worldˇ»
5372 "});
5373 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5374 cx.assert_editor_state(indoc! {"
5375 «HELLO WORLDˇ»
5376 "});
5377
5378 // If all upper case -> lower case
5379 cx.set_state(indoc! {"
5380 «HELLO WORLDˇ»
5381 "});
5382 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5383 cx.assert_editor_state(indoc! {"
5384 «hello worldˇ»
5385 "});
5386
5387 // If any upper case characters are identified -> lower case
5388 // This matches JetBrains IDEs
5389 cx.set_state(indoc! {"
5390 «hEllo worldˇ»
5391 "});
5392 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5393 cx.assert_editor_state(indoc! {"
5394 «hello worldˇ»
5395 "});
5396}
5397
5398#[gpui::test]
5399async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5400 init_test(cx, |_| {});
5401
5402 let mut cx = EditorTestContext::new(cx).await;
5403
5404 cx.set_state(indoc! {"
5405 «implement-windows-supportˇ»
5406 "});
5407 cx.update_editor(|e, window, cx| {
5408 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5409 });
5410 cx.assert_editor_state(indoc! {"
5411 «Implement windows supportˇ»
5412 "});
5413}
5414
5415#[gpui::test]
5416async fn test_manipulate_text(cx: &mut TestAppContext) {
5417 init_test(cx, |_| {});
5418
5419 let mut cx = EditorTestContext::new(cx).await;
5420
5421 // Test convert_to_upper_case()
5422 cx.set_state(indoc! {"
5423 «hello worldˇ»
5424 "});
5425 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5426 cx.assert_editor_state(indoc! {"
5427 «HELLO WORLDˇ»
5428 "});
5429
5430 // Test convert_to_lower_case()
5431 cx.set_state(indoc! {"
5432 «HELLO WORLDˇ»
5433 "});
5434 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5435 cx.assert_editor_state(indoc! {"
5436 «hello worldˇ»
5437 "});
5438
5439 // Test multiple line, single selection case
5440 cx.set_state(indoc! {"
5441 «The quick brown
5442 fox jumps over
5443 the lazy dogˇ»
5444 "});
5445 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5446 cx.assert_editor_state(indoc! {"
5447 «The Quick Brown
5448 Fox Jumps Over
5449 The Lazy Dogˇ»
5450 "});
5451
5452 // Test multiple line, single selection case
5453 cx.set_state(indoc! {"
5454 «The quick brown
5455 fox jumps over
5456 the lazy dogˇ»
5457 "});
5458 cx.update_editor(|e, window, cx| {
5459 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5460 });
5461 cx.assert_editor_state(indoc! {"
5462 «TheQuickBrown
5463 FoxJumpsOver
5464 TheLazyDogˇ»
5465 "});
5466
5467 // From here on out, test more complex cases of manipulate_text()
5468
5469 // Test no selection case - should affect words cursors are in
5470 // Cursor at beginning, middle, and end of word
5471 cx.set_state(indoc! {"
5472 ˇhello big beauˇtiful worldˇ
5473 "});
5474 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5475 cx.assert_editor_state(indoc! {"
5476 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5477 "});
5478
5479 // Test multiple selections on a single line and across multiple lines
5480 cx.set_state(indoc! {"
5481 «Theˇ» quick «brown
5482 foxˇ» jumps «overˇ»
5483 the «lazyˇ» dog
5484 "});
5485 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5486 cx.assert_editor_state(indoc! {"
5487 «THEˇ» quick «BROWN
5488 FOXˇ» jumps «OVERˇ»
5489 the «LAZYˇ» dog
5490 "});
5491
5492 // Test case where text length grows
5493 cx.set_state(indoc! {"
5494 «tschüߡ»
5495 "});
5496 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5497 cx.assert_editor_state(indoc! {"
5498 «TSCHÜSSˇ»
5499 "});
5500
5501 // Test to make sure we don't crash when text shrinks
5502 cx.set_state(indoc! {"
5503 aaa_bbbˇ
5504 "});
5505 cx.update_editor(|e, window, cx| {
5506 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5507 });
5508 cx.assert_editor_state(indoc! {"
5509 «aaaBbbˇ»
5510 "});
5511
5512 // Test to make sure we all aware of the fact that each word can grow and shrink
5513 // Final selections should be aware of this fact
5514 cx.set_state(indoc! {"
5515 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5516 "});
5517 cx.update_editor(|e, window, cx| {
5518 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5519 });
5520 cx.assert_editor_state(indoc! {"
5521 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5522 "});
5523
5524 cx.set_state(indoc! {"
5525 «hElLo, WoRld!ˇ»
5526 "});
5527 cx.update_editor(|e, window, cx| {
5528 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5529 });
5530 cx.assert_editor_state(indoc! {"
5531 «HeLlO, wOrLD!ˇ»
5532 "});
5533
5534 // Test selections with `line_mode() = true`.
5535 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5536 cx.set_state(indoc! {"
5537 «The quick brown
5538 fox jumps over
5539 tˇ»he lazy dog
5540 "});
5541 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5542 cx.assert_editor_state(indoc! {"
5543 «THE QUICK BROWN
5544 FOX JUMPS OVER
5545 THE LAZY DOGˇ»
5546 "});
5547}
5548
5549#[gpui::test]
5550fn test_duplicate_line(cx: &mut TestAppContext) {
5551 init_test(cx, |_| {});
5552
5553 let editor = cx.add_window(|window, cx| {
5554 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5555 build_editor(buffer, window, cx)
5556 });
5557 _ = editor.update(cx, |editor, window, cx| {
5558 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5559 s.select_display_ranges([
5560 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5561 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5562 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5563 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5564 ])
5565 });
5566 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5567 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5568 assert_eq!(
5569 editor.selections.display_ranges(cx),
5570 vec![
5571 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5572 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5573 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5574 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5575 ]
5576 );
5577 });
5578
5579 let editor = cx.add_window(|window, cx| {
5580 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5581 build_editor(buffer, window, cx)
5582 });
5583 _ = editor.update(cx, |editor, window, cx| {
5584 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5585 s.select_display_ranges([
5586 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5587 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5588 ])
5589 });
5590 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5591 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5592 assert_eq!(
5593 editor.selections.display_ranges(cx),
5594 vec![
5595 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5596 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5597 ]
5598 );
5599 });
5600
5601 // With `move_upwards` the selections stay in place, except for
5602 // the lines inserted above them
5603 let editor = cx.add_window(|window, cx| {
5604 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5605 build_editor(buffer, window, cx)
5606 });
5607 _ = editor.update(cx, |editor, window, cx| {
5608 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5609 s.select_display_ranges([
5610 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5611 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5612 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5613 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5614 ])
5615 });
5616 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5617 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5618 assert_eq!(
5619 editor.selections.display_ranges(cx),
5620 vec![
5621 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5622 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5623 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5624 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5625 ]
5626 );
5627 });
5628
5629 let editor = cx.add_window(|window, cx| {
5630 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5631 build_editor(buffer, window, cx)
5632 });
5633 _ = editor.update(cx, |editor, window, cx| {
5634 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5635 s.select_display_ranges([
5636 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5637 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5638 ])
5639 });
5640 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5641 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5642 assert_eq!(
5643 editor.selections.display_ranges(cx),
5644 vec![
5645 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5646 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5647 ]
5648 );
5649 });
5650
5651 let editor = cx.add_window(|window, cx| {
5652 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5653 build_editor(buffer, window, cx)
5654 });
5655 _ = editor.update(cx, |editor, window, cx| {
5656 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5657 s.select_display_ranges([
5658 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5659 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5660 ])
5661 });
5662 editor.duplicate_selection(&DuplicateSelection, window, cx);
5663 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5664 assert_eq!(
5665 editor.selections.display_ranges(cx),
5666 vec![
5667 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5668 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5669 ]
5670 );
5671 });
5672}
5673
5674#[gpui::test]
5675fn test_move_line_up_down(cx: &mut TestAppContext) {
5676 init_test(cx, |_| {});
5677
5678 let editor = cx.add_window(|window, cx| {
5679 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5680 build_editor(buffer, window, cx)
5681 });
5682 _ = editor.update(cx, |editor, window, cx| {
5683 editor.fold_creases(
5684 vec![
5685 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5686 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5687 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5688 ],
5689 true,
5690 window,
5691 cx,
5692 );
5693 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5694 s.select_display_ranges([
5695 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5696 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5697 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5698 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5699 ])
5700 });
5701 assert_eq!(
5702 editor.display_text(cx),
5703 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5704 );
5705
5706 editor.move_line_up(&MoveLineUp, window, cx);
5707 assert_eq!(
5708 editor.display_text(cx),
5709 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5710 );
5711 assert_eq!(
5712 editor.selections.display_ranges(cx),
5713 vec![
5714 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5715 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5716 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5717 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5718 ]
5719 );
5720 });
5721
5722 _ = editor.update(cx, |editor, window, cx| {
5723 editor.move_line_down(&MoveLineDown, window, cx);
5724 assert_eq!(
5725 editor.display_text(cx),
5726 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5727 );
5728 assert_eq!(
5729 editor.selections.display_ranges(cx),
5730 vec![
5731 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5732 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5733 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5734 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5735 ]
5736 );
5737 });
5738
5739 _ = editor.update(cx, |editor, window, cx| {
5740 editor.move_line_down(&MoveLineDown, window, cx);
5741 assert_eq!(
5742 editor.display_text(cx),
5743 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5744 );
5745 assert_eq!(
5746 editor.selections.display_ranges(cx),
5747 vec![
5748 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5749 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5750 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5751 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5752 ]
5753 );
5754 });
5755
5756 _ = editor.update(cx, |editor, window, cx| {
5757 editor.move_line_up(&MoveLineUp, window, cx);
5758 assert_eq!(
5759 editor.display_text(cx),
5760 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5761 );
5762 assert_eq!(
5763 editor.selections.display_ranges(cx),
5764 vec![
5765 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5766 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5767 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5768 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5769 ]
5770 );
5771 });
5772}
5773
5774#[gpui::test]
5775fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5776 init_test(cx, |_| {});
5777 let editor = cx.add_window(|window, cx| {
5778 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5779 build_editor(buffer, window, cx)
5780 });
5781 _ = editor.update(cx, |editor, window, cx| {
5782 editor.fold_creases(
5783 vec![Crease::simple(
5784 Point::new(6, 4)..Point::new(7, 4),
5785 FoldPlaceholder::test(),
5786 )],
5787 true,
5788 window,
5789 cx,
5790 );
5791 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5792 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5793 });
5794 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5795 editor.move_line_up(&MoveLineUp, window, cx);
5796 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5797 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5798 });
5799}
5800
5801#[gpui::test]
5802fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5803 init_test(cx, |_| {});
5804
5805 let editor = cx.add_window(|window, cx| {
5806 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5807 build_editor(buffer, window, cx)
5808 });
5809 _ = editor.update(cx, |editor, window, cx| {
5810 let snapshot = editor.buffer.read(cx).snapshot(cx);
5811 editor.insert_blocks(
5812 [BlockProperties {
5813 style: BlockStyle::Fixed,
5814 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5815 height: Some(1),
5816 render: Arc::new(|_| div().into_any()),
5817 priority: 0,
5818 }],
5819 Some(Autoscroll::fit()),
5820 cx,
5821 );
5822 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5823 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5824 });
5825 editor.move_line_down(&MoveLineDown, window, cx);
5826 });
5827}
5828
5829#[gpui::test]
5830async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5831 init_test(cx, |_| {});
5832
5833 let mut cx = EditorTestContext::new(cx).await;
5834 cx.set_state(
5835 &"
5836 ˇzero
5837 one
5838 two
5839 three
5840 four
5841 five
5842 "
5843 .unindent(),
5844 );
5845
5846 // Create a four-line block that replaces three lines of text.
5847 cx.update_editor(|editor, window, cx| {
5848 let snapshot = editor.snapshot(window, cx);
5849 let snapshot = &snapshot.buffer_snapshot();
5850 let placement = BlockPlacement::Replace(
5851 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5852 );
5853 editor.insert_blocks(
5854 [BlockProperties {
5855 placement,
5856 height: Some(4),
5857 style: BlockStyle::Sticky,
5858 render: Arc::new(|_| gpui::div().into_any_element()),
5859 priority: 0,
5860 }],
5861 None,
5862 cx,
5863 );
5864 });
5865
5866 // Move down so that the cursor touches the block.
5867 cx.update_editor(|editor, window, cx| {
5868 editor.move_down(&Default::default(), window, cx);
5869 });
5870 cx.assert_editor_state(
5871 &"
5872 zero
5873 «one
5874 two
5875 threeˇ»
5876 four
5877 five
5878 "
5879 .unindent(),
5880 );
5881
5882 // Move down past the block.
5883 cx.update_editor(|editor, window, cx| {
5884 editor.move_down(&Default::default(), window, cx);
5885 });
5886 cx.assert_editor_state(
5887 &"
5888 zero
5889 one
5890 two
5891 three
5892 ˇfour
5893 five
5894 "
5895 .unindent(),
5896 );
5897}
5898
5899#[gpui::test]
5900fn test_transpose(cx: &mut TestAppContext) {
5901 init_test(cx, |_| {});
5902
5903 _ = cx.add_window(|window, cx| {
5904 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5905 editor.set_style(EditorStyle::default(), window, cx);
5906 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5907 s.select_ranges([1..1])
5908 });
5909 editor.transpose(&Default::default(), window, cx);
5910 assert_eq!(editor.text(cx), "bac");
5911 assert_eq!(editor.selections.ranges(cx), [2..2]);
5912
5913 editor.transpose(&Default::default(), window, cx);
5914 assert_eq!(editor.text(cx), "bca");
5915 assert_eq!(editor.selections.ranges(cx), [3..3]);
5916
5917 editor.transpose(&Default::default(), window, cx);
5918 assert_eq!(editor.text(cx), "bac");
5919 assert_eq!(editor.selections.ranges(cx), [3..3]);
5920
5921 editor
5922 });
5923
5924 _ = cx.add_window(|window, cx| {
5925 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5926 editor.set_style(EditorStyle::default(), window, cx);
5927 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5928 s.select_ranges([3..3])
5929 });
5930 editor.transpose(&Default::default(), window, cx);
5931 assert_eq!(editor.text(cx), "acb\nde");
5932 assert_eq!(editor.selections.ranges(cx), [3..3]);
5933
5934 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5935 s.select_ranges([4..4])
5936 });
5937 editor.transpose(&Default::default(), window, cx);
5938 assert_eq!(editor.text(cx), "acbd\ne");
5939 assert_eq!(editor.selections.ranges(cx), [5..5]);
5940
5941 editor.transpose(&Default::default(), window, cx);
5942 assert_eq!(editor.text(cx), "acbde\n");
5943 assert_eq!(editor.selections.ranges(cx), [6..6]);
5944
5945 editor.transpose(&Default::default(), window, cx);
5946 assert_eq!(editor.text(cx), "acbd\ne");
5947 assert_eq!(editor.selections.ranges(cx), [6..6]);
5948
5949 editor
5950 });
5951
5952 _ = cx.add_window(|window, cx| {
5953 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5954 editor.set_style(EditorStyle::default(), window, cx);
5955 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5956 s.select_ranges([1..1, 2..2, 4..4])
5957 });
5958 editor.transpose(&Default::default(), window, cx);
5959 assert_eq!(editor.text(cx), "bacd\ne");
5960 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5961
5962 editor.transpose(&Default::default(), window, cx);
5963 assert_eq!(editor.text(cx), "bcade\n");
5964 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5965
5966 editor.transpose(&Default::default(), window, cx);
5967 assert_eq!(editor.text(cx), "bcda\ne");
5968 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5969
5970 editor.transpose(&Default::default(), window, cx);
5971 assert_eq!(editor.text(cx), "bcade\n");
5972 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5973
5974 editor.transpose(&Default::default(), window, cx);
5975 assert_eq!(editor.text(cx), "bcaed\n");
5976 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5977
5978 editor
5979 });
5980
5981 _ = cx.add_window(|window, cx| {
5982 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5983 editor.set_style(EditorStyle::default(), window, cx);
5984 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5985 s.select_ranges([4..4])
5986 });
5987 editor.transpose(&Default::default(), window, cx);
5988 assert_eq!(editor.text(cx), "🏀🍐✋");
5989 assert_eq!(editor.selections.ranges(cx), [8..8]);
5990
5991 editor.transpose(&Default::default(), window, cx);
5992 assert_eq!(editor.text(cx), "🏀✋🍐");
5993 assert_eq!(editor.selections.ranges(cx), [11..11]);
5994
5995 editor.transpose(&Default::default(), window, cx);
5996 assert_eq!(editor.text(cx), "🏀🍐✋");
5997 assert_eq!(editor.selections.ranges(cx), [11..11]);
5998
5999 editor
6000 });
6001}
6002
6003#[gpui::test]
6004async fn test_rewrap(cx: &mut TestAppContext) {
6005 init_test(cx, |settings| {
6006 settings.languages.0.extend([
6007 (
6008 "Markdown".into(),
6009 LanguageSettingsContent {
6010 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6011 preferred_line_length: Some(40),
6012 ..Default::default()
6013 },
6014 ),
6015 (
6016 "Plain Text".into(),
6017 LanguageSettingsContent {
6018 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6019 preferred_line_length: Some(40),
6020 ..Default::default()
6021 },
6022 ),
6023 (
6024 "C++".into(),
6025 LanguageSettingsContent {
6026 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6027 preferred_line_length: Some(40),
6028 ..Default::default()
6029 },
6030 ),
6031 (
6032 "Python".into(),
6033 LanguageSettingsContent {
6034 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6035 preferred_line_length: Some(40),
6036 ..Default::default()
6037 },
6038 ),
6039 (
6040 "Rust".into(),
6041 LanguageSettingsContent {
6042 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6043 preferred_line_length: Some(40),
6044 ..Default::default()
6045 },
6046 ),
6047 ])
6048 });
6049
6050 let mut cx = EditorTestContext::new(cx).await;
6051
6052 let cpp_language = Arc::new(Language::new(
6053 LanguageConfig {
6054 name: "C++".into(),
6055 line_comments: vec!["// ".into()],
6056 ..LanguageConfig::default()
6057 },
6058 None,
6059 ));
6060 let python_language = Arc::new(Language::new(
6061 LanguageConfig {
6062 name: "Python".into(),
6063 line_comments: vec!["# ".into()],
6064 ..LanguageConfig::default()
6065 },
6066 None,
6067 ));
6068 let markdown_language = Arc::new(Language::new(
6069 LanguageConfig {
6070 name: "Markdown".into(),
6071 rewrap_prefixes: vec![
6072 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6073 regex::Regex::new("[-*+]\\s+").unwrap(),
6074 ],
6075 ..LanguageConfig::default()
6076 },
6077 None,
6078 ));
6079 let rust_language = Arc::new(
6080 Language::new(
6081 LanguageConfig {
6082 name: "Rust".into(),
6083 line_comments: vec!["// ".into(), "/// ".into()],
6084 ..LanguageConfig::default()
6085 },
6086 Some(tree_sitter_rust::LANGUAGE.into()),
6087 )
6088 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6089 .unwrap(),
6090 );
6091
6092 let plaintext_language = Arc::new(Language::new(
6093 LanguageConfig {
6094 name: "Plain Text".into(),
6095 ..LanguageConfig::default()
6096 },
6097 None,
6098 ));
6099
6100 // Test basic rewrapping of a long line with a cursor
6101 assert_rewrap(
6102 indoc! {"
6103 // ˇThis is a long comment that needs to be wrapped.
6104 "},
6105 indoc! {"
6106 // ˇThis is a long comment that needs to
6107 // be wrapped.
6108 "},
6109 cpp_language.clone(),
6110 &mut cx,
6111 );
6112
6113 // Test rewrapping a full selection
6114 assert_rewrap(
6115 indoc! {"
6116 «// This selected long comment needs to be wrapped.ˇ»"
6117 },
6118 indoc! {"
6119 «// This selected long comment needs to
6120 // be wrapped.ˇ»"
6121 },
6122 cpp_language.clone(),
6123 &mut cx,
6124 );
6125
6126 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6127 assert_rewrap(
6128 indoc! {"
6129 // ˇThis is the first line.
6130 // Thisˇ is the second line.
6131 // This is the thirdˇ line, all part of one paragraph.
6132 "},
6133 indoc! {"
6134 // ˇThis is the first line. Thisˇ is the
6135 // second line. This is the thirdˇ line,
6136 // all part of one paragraph.
6137 "},
6138 cpp_language.clone(),
6139 &mut cx,
6140 );
6141
6142 // Test multiple cursors in different paragraphs trigger separate rewraps
6143 assert_rewrap(
6144 indoc! {"
6145 // ˇThis is the first paragraph, first line.
6146 // ˇThis is the first paragraph, second line.
6147
6148 // ˇThis is the second paragraph, first line.
6149 // ˇThis is the second paragraph, second line.
6150 "},
6151 indoc! {"
6152 // ˇThis is the first paragraph, first
6153 // line. ˇThis is the first paragraph,
6154 // second line.
6155
6156 // ˇThis is the second paragraph, first
6157 // line. ˇThis is the second paragraph,
6158 // second line.
6159 "},
6160 cpp_language.clone(),
6161 &mut cx,
6162 );
6163
6164 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6165 assert_rewrap(
6166 indoc! {"
6167 «// A regular long long comment to be wrapped.
6168 /// A documentation long comment to be wrapped.ˇ»
6169 "},
6170 indoc! {"
6171 «// A regular long long comment to be
6172 // wrapped.
6173 /// A documentation long comment to be
6174 /// wrapped.ˇ»
6175 "},
6176 rust_language.clone(),
6177 &mut cx,
6178 );
6179
6180 // Test that change in indentation level trigger seperate rewraps
6181 assert_rewrap(
6182 indoc! {"
6183 fn foo() {
6184 «// This is a long comment at the base indent.
6185 // This is a long comment at the next indent.ˇ»
6186 }
6187 "},
6188 indoc! {"
6189 fn foo() {
6190 «// This is a long comment at the
6191 // base indent.
6192 // This is a long comment at the
6193 // next indent.ˇ»
6194 }
6195 "},
6196 rust_language.clone(),
6197 &mut cx,
6198 );
6199
6200 // Test that different comment prefix characters (e.g., '#') are handled correctly
6201 assert_rewrap(
6202 indoc! {"
6203 # ˇThis is a long comment using a pound sign.
6204 "},
6205 indoc! {"
6206 # ˇThis is a long comment using a pound
6207 # sign.
6208 "},
6209 python_language,
6210 &mut cx,
6211 );
6212
6213 // Test rewrapping only affects comments, not code even when selected
6214 assert_rewrap(
6215 indoc! {"
6216 «/// This doc comment is long and should be wrapped.
6217 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6218 "},
6219 indoc! {"
6220 «/// This doc comment is long and should
6221 /// be wrapped.
6222 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6223 "},
6224 rust_language.clone(),
6225 &mut cx,
6226 );
6227
6228 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6229 assert_rewrap(
6230 indoc! {"
6231 # Header
6232
6233 A long long long line of markdown text to wrap.ˇ
6234 "},
6235 indoc! {"
6236 # Header
6237
6238 A long long long line of markdown text
6239 to wrap.ˇ
6240 "},
6241 markdown_language.clone(),
6242 &mut cx,
6243 );
6244
6245 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6246 assert_rewrap(
6247 indoc! {"
6248 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6249 2. This is a numbered list item that is very long and needs to be wrapped properly.
6250 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6251 "},
6252 indoc! {"
6253 «1. This is a numbered list item that is
6254 very long and needs to be wrapped
6255 properly.
6256 2. This is a numbered list item that is
6257 very long and needs to be wrapped
6258 properly.
6259 - This is an unordered list item that is
6260 also very long and should not merge
6261 with the numbered item.ˇ»
6262 "},
6263 markdown_language.clone(),
6264 &mut cx,
6265 );
6266
6267 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6268 assert_rewrap(
6269 indoc! {"
6270 «1. This is a numbered list item that is
6271 very long and needs to be wrapped
6272 properly.
6273 2. This is a numbered list item that is
6274 very long and needs to be wrapped
6275 properly.
6276 - This is an unordered list item that is
6277 also very long and should not merge with
6278 the numbered item.ˇ»
6279 "},
6280 indoc! {"
6281 «1. This is a numbered list item that is
6282 very long and needs to be wrapped
6283 properly.
6284 2. This is a numbered list item that is
6285 very long and needs to be wrapped
6286 properly.
6287 - This is an unordered list item that is
6288 also very long and should not merge
6289 with the numbered item.ˇ»
6290 "},
6291 markdown_language.clone(),
6292 &mut cx,
6293 );
6294
6295 // Test that rewrapping maintain indents even when they already exists.
6296 assert_rewrap(
6297 indoc! {"
6298 «1. This is a numbered list
6299 item that is very long and needs to be wrapped properly.
6300 2. This is a numbered list
6301 item that is very long and needs to be wrapped properly.
6302 - This is an unordered list item that is also very long and
6303 should not merge with the numbered item.ˇ»
6304 "},
6305 indoc! {"
6306 «1. This is a numbered list item that is
6307 very long and needs to be wrapped
6308 properly.
6309 2. This is a numbered list item that is
6310 very long and needs to be wrapped
6311 properly.
6312 - This is an unordered list item that is
6313 also very long and should not merge
6314 with the numbered item.ˇ»
6315 "},
6316 markdown_language,
6317 &mut cx,
6318 );
6319
6320 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6321 assert_rewrap(
6322 indoc! {"
6323 ˇThis is a very long line of plain text that will be wrapped.
6324 "},
6325 indoc! {"
6326 ˇThis is a very long line of plain text
6327 that will be wrapped.
6328 "},
6329 plaintext_language.clone(),
6330 &mut cx,
6331 );
6332
6333 // Test that non-commented code acts as a paragraph boundary within a selection
6334 assert_rewrap(
6335 indoc! {"
6336 «// This is the first long comment block to be wrapped.
6337 fn my_func(a: u32);
6338 // This is the second long comment block to be wrapped.ˇ»
6339 "},
6340 indoc! {"
6341 «// This is the first long comment block
6342 // to be wrapped.
6343 fn my_func(a: u32);
6344 // This is the second long comment block
6345 // to be wrapped.ˇ»
6346 "},
6347 rust_language,
6348 &mut cx,
6349 );
6350
6351 // Test rewrapping multiple selections, including ones with blank lines or tabs
6352 assert_rewrap(
6353 indoc! {"
6354 «ˇThis is a very long line that will be wrapped.
6355
6356 This is another paragraph in the same selection.»
6357
6358 «\tThis is a very long indented line that will be wrapped.ˇ»
6359 "},
6360 indoc! {"
6361 «ˇThis is a very long line that will be
6362 wrapped.
6363
6364 This is another paragraph in the same
6365 selection.»
6366
6367 «\tThis is a very long indented line
6368 \tthat will be wrapped.ˇ»
6369 "},
6370 plaintext_language,
6371 &mut cx,
6372 );
6373
6374 // Test that an empty comment line acts as a paragraph boundary
6375 assert_rewrap(
6376 indoc! {"
6377 // ˇThis is a long comment that will be wrapped.
6378 //
6379 // And this is another long comment that will also be wrapped.ˇ
6380 "},
6381 indoc! {"
6382 // ˇThis is a long comment that will be
6383 // wrapped.
6384 //
6385 // And this is another long comment that
6386 // will also be wrapped.ˇ
6387 "},
6388 cpp_language,
6389 &mut cx,
6390 );
6391
6392 #[track_caller]
6393 fn assert_rewrap(
6394 unwrapped_text: &str,
6395 wrapped_text: &str,
6396 language: Arc<Language>,
6397 cx: &mut EditorTestContext,
6398 ) {
6399 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6400 cx.set_state(unwrapped_text);
6401 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6402 cx.assert_editor_state(wrapped_text);
6403 }
6404}
6405
6406#[gpui::test]
6407async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6408 init_test(cx, |settings| {
6409 settings.languages.0.extend([(
6410 "Rust".into(),
6411 LanguageSettingsContent {
6412 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6413 preferred_line_length: Some(40),
6414 ..Default::default()
6415 },
6416 )])
6417 });
6418
6419 let mut cx = EditorTestContext::new(cx).await;
6420
6421 let rust_lang = Arc::new(
6422 Language::new(
6423 LanguageConfig {
6424 name: "Rust".into(),
6425 line_comments: vec!["// ".into()],
6426 block_comment: Some(BlockCommentConfig {
6427 start: "/*".into(),
6428 end: "*/".into(),
6429 prefix: "* ".into(),
6430 tab_size: 1,
6431 }),
6432 documentation_comment: Some(BlockCommentConfig {
6433 start: "/**".into(),
6434 end: "*/".into(),
6435 prefix: "* ".into(),
6436 tab_size: 1,
6437 }),
6438
6439 ..LanguageConfig::default()
6440 },
6441 Some(tree_sitter_rust::LANGUAGE.into()),
6442 )
6443 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6444 .unwrap(),
6445 );
6446
6447 // regular block comment
6448 assert_rewrap(
6449 indoc! {"
6450 /*
6451 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6452 */
6453 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6454 "},
6455 indoc! {"
6456 /*
6457 *ˇ Lorem ipsum dolor sit amet,
6458 * consectetur adipiscing elit.
6459 */
6460 /*
6461 *ˇ Lorem ipsum dolor sit amet,
6462 * consectetur adipiscing elit.
6463 */
6464 "},
6465 rust_lang.clone(),
6466 &mut cx,
6467 );
6468
6469 // indent is respected
6470 assert_rewrap(
6471 indoc! {"
6472 {}
6473 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6474 "},
6475 indoc! {"
6476 {}
6477 /*
6478 *ˇ Lorem ipsum dolor sit amet,
6479 * consectetur adipiscing elit.
6480 */
6481 "},
6482 rust_lang.clone(),
6483 &mut cx,
6484 );
6485
6486 // short block comments with inline delimiters
6487 assert_rewrap(
6488 indoc! {"
6489 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6490 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6491 */
6492 /*
6493 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6494 "},
6495 indoc! {"
6496 /*
6497 *ˇ Lorem ipsum dolor sit amet,
6498 * consectetur adipiscing elit.
6499 */
6500 /*
6501 *ˇ Lorem ipsum dolor sit amet,
6502 * consectetur adipiscing elit.
6503 */
6504 /*
6505 *ˇ Lorem ipsum dolor sit amet,
6506 * consectetur adipiscing elit.
6507 */
6508 "},
6509 rust_lang.clone(),
6510 &mut cx,
6511 );
6512
6513 // multiline block comment with inline start/end delimiters
6514 assert_rewrap(
6515 indoc! {"
6516 /*ˇ Lorem ipsum dolor sit amet,
6517 * consectetur adipiscing elit. */
6518 "},
6519 indoc! {"
6520 /*
6521 *ˇ Lorem ipsum dolor sit amet,
6522 * consectetur adipiscing elit.
6523 */
6524 "},
6525 rust_lang.clone(),
6526 &mut cx,
6527 );
6528
6529 // block comment rewrap still respects paragraph bounds
6530 assert_rewrap(
6531 indoc! {"
6532 /*
6533 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6534 *
6535 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6536 */
6537 "},
6538 indoc! {"
6539 /*
6540 *ˇ Lorem ipsum dolor sit amet,
6541 * consectetur adipiscing elit.
6542 *
6543 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6544 */
6545 "},
6546 rust_lang.clone(),
6547 &mut cx,
6548 );
6549
6550 // documentation comments
6551 assert_rewrap(
6552 indoc! {"
6553 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6554 /**
6555 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6556 */
6557 "},
6558 indoc! {"
6559 /**
6560 *ˇ Lorem ipsum dolor sit amet,
6561 * consectetur adipiscing elit.
6562 */
6563 /**
6564 *ˇ Lorem ipsum dolor sit amet,
6565 * consectetur adipiscing elit.
6566 */
6567 "},
6568 rust_lang.clone(),
6569 &mut cx,
6570 );
6571
6572 // different, adjacent comments
6573 assert_rewrap(
6574 indoc! {"
6575 /**
6576 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6577 */
6578 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6579 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6580 "},
6581 indoc! {"
6582 /**
6583 *ˇ Lorem ipsum dolor sit amet,
6584 * consectetur adipiscing elit.
6585 */
6586 /*
6587 *ˇ Lorem ipsum dolor sit amet,
6588 * consectetur adipiscing elit.
6589 */
6590 //ˇ Lorem ipsum dolor sit amet,
6591 // consectetur adipiscing elit.
6592 "},
6593 rust_lang.clone(),
6594 &mut cx,
6595 );
6596
6597 // selection w/ single short block comment
6598 assert_rewrap(
6599 indoc! {"
6600 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6601 "},
6602 indoc! {"
6603 «/*
6604 * Lorem ipsum dolor sit amet,
6605 * consectetur adipiscing elit.
6606 */ˇ»
6607 "},
6608 rust_lang.clone(),
6609 &mut cx,
6610 );
6611
6612 // rewrapping a single comment w/ abutting comments
6613 assert_rewrap(
6614 indoc! {"
6615 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6616 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6617 "},
6618 indoc! {"
6619 /*
6620 * ˇLorem ipsum dolor sit amet,
6621 * consectetur adipiscing elit.
6622 */
6623 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6624 "},
6625 rust_lang.clone(),
6626 &mut cx,
6627 );
6628
6629 // selection w/ non-abutting short block comments
6630 assert_rewrap(
6631 indoc! {"
6632 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6633
6634 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6635 "},
6636 indoc! {"
6637 «/*
6638 * Lorem ipsum dolor sit amet,
6639 * consectetur adipiscing elit.
6640 */
6641
6642 /*
6643 * Lorem ipsum dolor sit amet,
6644 * consectetur adipiscing elit.
6645 */ˇ»
6646 "},
6647 rust_lang.clone(),
6648 &mut cx,
6649 );
6650
6651 // selection of multiline block comments
6652 assert_rewrap(
6653 indoc! {"
6654 «/* Lorem ipsum dolor sit amet,
6655 * consectetur adipiscing elit. */ˇ»
6656 "},
6657 indoc! {"
6658 «/*
6659 * Lorem ipsum dolor sit amet,
6660 * consectetur adipiscing elit.
6661 */ˇ»
6662 "},
6663 rust_lang.clone(),
6664 &mut cx,
6665 );
6666
6667 // partial selection of multiline block comments
6668 assert_rewrap(
6669 indoc! {"
6670 «/* Lorem ipsum dolor sit amet,ˇ»
6671 * consectetur adipiscing elit. */
6672 /* Lorem ipsum dolor sit amet,
6673 «* consectetur adipiscing elit. */ˇ»
6674 "},
6675 indoc! {"
6676 «/*
6677 * Lorem ipsum dolor sit amet,ˇ»
6678 * consectetur adipiscing elit. */
6679 /* Lorem ipsum dolor sit amet,
6680 «* consectetur adipiscing elit.
6681 */ˇ»
6682 "},
6683 rust_lang.clone(),
6684 &mut cx,
6685 );
6686
6687 // selection w/ abutting short block comments
6688 // TODO: should not be combined; should rewrap as 2 comments
6689 assert_rewrap(
6690 indoc! {"
6691 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6692 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6693 "},
6694 // desired behavior:
6695 // indoc! {"
6696 // «/*
6697 // * Lorem ipsum dolor sit amet,
6698 // * consectetur adipiscing elit.
6699 // */
6700 // /*
6701 // * Lorem ipsum dolor sit amet,
6702 // * consectetur adipiscing elit.
6703 // */ˇ»
6704 // "},
6705 // actual behaviour:
6706 indoc! {"
6707 «/*
6708 * Lorem ipsum dolor sit amet,
6709 * consectetur adipiscing elit. Lorem
6710 * ipsum dolor sit amet, consectetur
6711 * adipiscing elit.
6712 */ˇ»
6713 "},
6714 rust_lang.clone(),
6715 &mut cx,
6716 );
6717
6718 // TODO: same as above, but with delimiters on separate line
6719 // assert_rewrap(
6720 // indoc! {"
6721 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6722 // */
6723 // /*
6724 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6725 // "},
6726 // // desired:
6727 // // indoc! {"
6728 // // «/*
6729 // // * Lorem ipsum dolor sit amet,
6730 // // * consectetur adipiscing elit.
6731 // // */
6732 // // /*
6733 // // * Lorem ipsum dolor sit amet,
6734 // // * consectetur adipiscing elit.
6735 // // */ˇ»
6736 // // "},
6737 // // actual: (but with trailing w/s on the empty lines)
6738 // indoc! {"
6739 // «/*
6740 // * Lorem ipsum dolor sit amet,
6741 // * consectetur adipiscing elit.
6742 // *
6743 // */
6744 // /*
6745 // *
6746 // * Lorem ipsum dolor sit amet,
6747 // * consectetur adipiscing elit.
6748 // */ˇ»
6749 // "},
6750 // rust_lang.clone(),
6751 // &mut cx,
6752 // );
6753
6754 // TODO these are unhandled edge cases; not correct, just documenting known issues
6755 assert_rewrap(
6756 indoc! {"
6757 /*
6758 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6759 */
6760 /*
6761 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6762 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6763 "},
6764 // desired:
6765 // indoc! {"
6766 // /*
6767 // *ˇ Lorem ipsum dolor sit amet,
6768 // * consectetur adipiscing elit.
6769 // */
6770 // /*
6771 // *ˇ Lorem ipsum dolor sit amet,
6772 // * consectetur adipiscing elit.
6773 // */
6774 // /*
6775 // *ˇ Lorem ipsum dolor sit amet
6776 // */ /* consectetur adipiscing elit. */
6777 // "},
6778 // actual:
6779 indoc! {"
6780 /*
6781 //ˇ Lorem ipsum dolor sit amet,
6782 // consectetur adipiscing elit.
6783 */
6784 /*
6785 * //ˇ Lorem ipsum dolor sit amet,
6786 * consectetur adipiscing elit.
6787 */
6788 /*
6789 *ˇ Lorem ipsum dolor sit amet */ /*
6790 * consectetur adipiscing elit.
6791 */
6792 "},
6793 rust_lang,
6794 &mut cx,
6795 );
6796
6797 #[track_caller]
6798 fn assert_rewrap(
6799 unwrapped_text: &str,
6800 wrapped_text: &str,
6801 language: Arc<Language>,
6802 cx: &mut EditorTestContext,
6803 ) {
6804 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6805 cx.set_state(unwrapped_text);
6806 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6807 cx.assert_editor_state(wrapped_text);
6808 }
6809}
6810
6811#[gpui::test]
6812async fn test_hard_wrap(cx: &mut TestAppContext) {
6813 init_test(cx, |_| {});
6814 let mut cx = EditorTestContext::new(cx).await;
6815
6816 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6817 cx.update_editor(|editor, _, cx| {
6818 editor.set_hard_wrap(Some(14), cx);
6819 });
6820
6821 cx.set_state(indoc!(
6822 "
6823 one two three ˇ
6824 "
6825 ));
6826 cx.simulate_input("four");
6827 cx.run_until_parked();
6828
6829 cx.assert_editor_state(indoc!(
6830 "
6831 one two three
6832 fourˇ
6833 "
6834 ));
6835
6836 cx.update_editor(|editor, window, cx| {
6837 editor.newline(&Default::default(), window, cx);
6838 });
6839 cx.run_until_parked();
6840 cx.assert_editor_state(indoc!(
6841 "
6842 one two three
6843 four
6844 ˇ
6845 "
6846 ));
6847
6848 cx.simulate_input("five");
6849 cx.run_until_parked();
6850 cx.assert_editor_state(indoc!(
6851 "
6852 one two three
6853 four
6854 fiveˇ
6855 "
6856 ));
6857
6858 cx.update_editor(|editor, window, cx| {
6859 editor.newline(&Default::default(), window, cx);
6860 });
6861 cx.run_until_parked();
6862 cx.simulate_input("# ");
6863 cx.run_until_parked();
6864 cx.assert_editor_state(indoc!(
6865 "
6866 one two three
6867 four
6868 five
6869 # ˇ
6870 "
6871 ));
6872
6873 cx.update_editor(|editor, window, cx| {
6874 editor.newline(&Default::default(), window, cx);
6875 });
6876 cx.run_until_parked();
6877 cx.assert_editor_state(indoc!(
6878 "
6879 one two three
6880 four
6881 five
6882 #\x20
6883 #ˇ
6884 "
6885 ));
6886
6887 cx.simulate_input(" 6");
6888 cx.run_until_parked();
6889 cx.assert_editor_state(indoc!(
6890 "
6891 one two three
6892 four
6893 five
6894 #
6895 # 6ˇ
6896 "
6897 ));
6898}
6899
6900#[gpui::test]
6901async fn test_cut_line_ends(cx: &mut TestAppContext) {
6902 init_test(cx, |_| {});
6903
6904 let mut cx = EditorTestContext::new(cx).await;
6905
6906 cx.set_state(indoc! {"The quick brownˇ"});
6907 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6908 cx.assert_editor_state(indoc! {"The quick brownˇ"});
6909
6910 cx.set_state(indoc! {"The emacs foxˇ"});
6911 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6912 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
6913
6914 cx.set_state(indoc! {"
6915 The quick« brownˇ»
6916 fox jumps overˇ
6917 the lazy dog"});
6918 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6919 cx.assert_editor_state(indoc! {"
6920 The quickˇ
6921 ˇthe lazy dog"});
6922
6923 cx.set_state(indoc! {"
6924 The quick« brownˇ»
6925 fox jumps overˇ
6926 the lazy dog"});
6927 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6928 cx.assert_editor_state(indoc! {"
6929 The quickˇ
6930 fox jumps overˇthe lazy dog"});
6931
6932 cx.set_state(indoc! {"
6933 The quick« brownˇ»
6934 fox jumps overˇ
6935 the lazy dog"});
6936 cx.update_editor(|e, window, cx| {
6937 e.cut_to_end_of_line(
6938 &CutToEndOfLine {
6939 stop_at_newlines: true,
6940 },
6941 window,
6942 cx,
6943 )
6944 });
6945 cx.assert_editor_state(indoc! {"
6946 The quickˇ
6947 fox jumps overˇ
6948 the lazy dog"});
6949
6950 cx.set_state(indoc! {"
6951 The quick« brownˇ»
6952 fox jumps overˇ
6953 the lazy dog"});
6954 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6955 cx.assert_editor_state(indoc! {"
6956 The quickˇ
6957 fox jumps overˇthe lazy dog"});
6958}
6959
6960#[gpui::test]
6961async fn test_clipboard(cx: &mut TestAppContext) {
6962 init_test(cx, |_| {});
6963
6964 let mut cx = EditorTestContext::new(cx).await;
6965
6966 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
6967 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6968 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
6969
6970 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
6971 cx.set_state("two ˇfour ˇsix ˇ");
6972 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6973 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
6974
6975 // Paste again but with only two cursors. Since the number of cursors doesn't
6976 // match the number of slices in the clipboard, the entire clipboard text
6977 // is pasted at each cursor.
6978 cx.set_state("ˇtwo one✅ four three six five ˇ");
6979 cx.update_editor(|e, window, cx| {
6980 e.handle_input("( ", window, cx);
6981 e.paste(&Paste, window, cx);
6982 e.handle_input(") ", window, cx);
6983 });
6984 cx.assert_editor_state(
6985 &([
6986 "( one✅ ",
6987 "three ",
6988 "five ) ˇtwo one✅ four three six five ( one✅ ",
6989 "three ",
6990 "five ) ˇ",
6991 ]
6992 .join("\n")),
6993 );
6994
6995 // Cut with three selections, one of which is full-line.
6996 cx.set_state(indoc! {"
6997 1«2ˇ»3
6998 4ˇ567
6999 «8ˇ»9"});
7000 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7001 cx.assert_editor_state(indoc! {"
7002 1ˇ3
7003 ˇ9"});
7004
7005 // Paste with three selections, noticing how the copied selection that was full-line
7006 // gets inserted before the second cursor.
7007 cx.set_state(indoc! {"
7008 1ˇ3
7009 9ˇ
7010 «oˇ»ne"});
7011 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7012 cx.assert_editor_state(indoc! {"
7013 12ˇ3
7014 4567
7015 9ˇ
7016 8ˇne"});
7017
7018 // Copy with a single cursor only, which writes the whole line into the clipboard.
7019 cx.set_state(indoc! {"
7020 The quick brown
7021 fox juˇmps over
7022 the lazy dog"});
7023 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7024 assert_eq!(
7025 cx.read_from_clipboard()
7026 .and_then(|item| item.text().as_deref().map(str::to_string)),
7027 Some("fox jumps over\n".to_string())
7028 );
7029
7030 // Paste with three selections, noticing how the copied full-line selection is inserted
7031 // before the empty selections but replaces the selection that is non-empty.
7032 cx.set_state(indoc! {"
7033 Tˇhe quick brown
7034 «foˇ»x jumps over
7035 tˇhe lazy dog"});
7036 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7037 cx.assert_editor_state(indoc! {"
7038 fox jumps over
7039 Tˇhe quick brown
7040 fox jumps over
7041 ˇx jumps over
7042 fox jumps over
7043 tˇhe lazy dog"});
7044}
7045
7046#[gpui::test]
7047async fn test_copy_trim(cx: &mut TestAppContext) {
7048 init_test(cx, |_| {});
7049
7050 let mut cx = EditorTestContext::new(cx).await;
7051 cx.set_state(
7052 r#" «for selection in selections.iter() {
7053 let mut start = selection.start;
7054 let mut end = selection.end;
7055 let is_entire_line = selection.is_empty();
7056 if is_entire_line {
7057 start = Point::new(start.row, 0);ˇ»
7058 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7059 }
7060 "#,
7061 );
7062 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7063 assert_eq!(
7064 cx.read_from_clipboard()
7065 .and_then(|item| item.text().as_deref().map(str::to_string)),
7066 Some(
7067 "for selection in selections.iter() {
7068 let mut start = selection.start;
7069 let mut end = selection.end;
7070 let is_entire_line = selection.is_empty();
7071 if is_entire_line {
7072 start = Point::new(start.row, 0);"
7073 .to_string()
7074 ),
7075 "Regular copying preserves all indentation selected",
7076 );
7077 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7078 assert_eq!(
7079 cx.read_from_clipboard()
7080 .and_then(|item| item.text().as_deref().map(str::to_string)),
7081 Some(
7082 "for selection in selections.iter() {
7083let mut start = selection.start;
7084let mut end = selection.end;
7085let is_entire_line = selection.is_empty();
7086if is_entire_line {
7087 start = Point::new(start.row, 0);"
7088 .to_string()
7089 ),
7090 "Copying with stripping should strip all leading whitespaces"
7091 );
7092
7093 cx.set_state(
7094 r#" « for selection in selections.iter() {
7095 let mut start = selection.start;
7096 let mut end = selection.end;
7097 let is_entire_line = selection.is_empty();
7098 if is_entire_line {
7099 start = Point::new(start.row, 0);ˇ»
7100 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7101 }
7102 "#,
7103 );
7104 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7105 assert_eq!(
7106 cx.read_from_clipboard()
7107 .and_then(|item| item.text().as_deref().map(str::to_string)),
7108 Some(
7109 " for selection in selections.iter() {
7110 let mut start = selection.start;
7111 let mut end = selection.end;
7112 let is_entire_line = selection.is_empty();
7113 if is_entire_line {
7114 start = Point::new(start.row, 0);"
7115 .to_string()
7116 ),
7117 "Regular copying preserves all indentation selected",
7118 );
7119 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7120 assert_eq!(
7121 cx.read_from_clipboard()
7122 .and_then(|item| item.text().as_deref().map(str::to_string)),
7123 Some(
7124 "for selection in selections.iter() {
7125let mut start = selection.start;
7126let mut end = selection.end;
7127let is_entire_line = selection.is_empty();
7128if is_entire_line {
7129 start = Point::new(start.row, 0);"
7130 .to_string()
7131 ),
7132 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7133 );
7134
7135 cx.set_state(
7136 r#" «ˇ for selection in selections.iter() {
7137 let mut start = selection.start;
7138 let mut end = selection.end;
7139 let is_entire_line = selection.is_empty();
7140 if is_entire_line {
7141 start = Point::new(start.row, 0);»
7142 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7143 }
7144 "#,
7145 );
7146 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7147 assert_eq!(
7148 cx.read_from_clipboard()
7149 .and_then(|item| item.text().as_deref().map(str::to_string)),
7150 Some(
7151 " for selection in selections.iter() {
7152 let mut start = selection.start;
7153 let mut end = selection.end;
7154 let is_entire_line = selection.is_empty();
7155 if is_entire_line {
7156 start = Point::new(start.row, 0);"
7157 .to_string()
7158 ),
7159 "Regular copying for reverse selection works the same",
7160 );
7161 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7162 assert_eq!(
7163 cx.read_from_clipboard()
7164 .and_then(|item| item.text().as_deref().map(str::to_string)),
7165 Some(
7166 "for selection in selections.iter() {
7167let mut start = selection.start;
7168let mut end = selection.end;
7169let is_entire_line = selection.is_empty();
7170if is_entire_line {
7171 start = Point::new(start.row, 0);"
7172 .to_string()
7173 ),
7174 "Copying with stripping for reverse selection works the same"
7175 );
7176
7177 cx.set_state(
7178 r#" for selection «in selections.iter() {
7179 let mut start = selection.start;
7180 let mut end = selection.end;
7181 let is_entire_line = selection.is_empty();
7182 if is_entire_line {
7183 start = Point::new(start.row, 0);ˇ»
7184 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7185 }
7186 "#,
7187 );
7188 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7189 assert_eq!(
7190 cx.read_from_clipboard()
7191 .and_then(|item| item.text().as_deref().map(str::to_string)),
7192 Some(
7193 "in selections.iter() {
7194 let mut start = selection.start;
7195 let mut end = selection.end;
7196 let is_entire_line = selection.is_empty();
7197 if is_entire_line {
7198 start = Point::new(start.row, 0);"
7199 .to_string()
7200 ),
7201 "When selecting past the indent, the copying works as usual",
7202 );
7203 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7204 assert_eq!(
7205 cx.read_from_clipboard()
7206 .and_then(|item| item.text().as_deref().map(str::to_string)),
7207 Some(
7208 "in selections.iter() {
7209 let mut start = selection.start;
7210 let mut end = selection.end;
7211 let is_entire_line = selection.is_empty();
7212 if is_entire_line {
7213 start = Point::new(start.row, 0);"
7214 .to_string()
7215 ),
7216 "When selecting past the indent, nothing is trimmed"
7217 );
7218
7219 cx.set_state(
7220 r#" «for selection in selections.iter() {
7221 let mut start = selection.start;
7222
7223 let mut end = selection.end;
7224 let is_entire_line = selection.is_empty();
7225 if is_entire_line {
7226 start = Point::new(start.row, 0);
7227ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7228 }
7229 "#,
7230 );
7231 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7232 assert_eq!(
7233 cx.read_from_clipboard()
7234 .and_then(|item| item.text().as_deref().map(str::to_string)),
7235 Some(
7236 "for selection in selections.iter() {
7237let mut start = selection.start;
7238
7239let mut end = selection.end;
7240let is_entire_line = selection.is_empty();
7241if is_entire_line {
7242 start = Point::new(start.row, 0);
7243"
7244 .to_string()
7245 ),
7246 "Copying with stripping should ignore empty lines"
7247 );
7248}
7249
7250#[gpui::test]
7251async fn test_paste_multiline(cx: &mut TestAppContext) {
7252 init_test(cx, |_| {});
7253
7254 let mut cx = EditorTestContext::new(cx).await;
7255 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7256
7257 // Cut an indented block, without the leading whitespace.
7258 cx.set_state(indoc! {"
7259 const a: B = (
7260 c(),
7261 «d(
7262 e,
7263 f
7264 )ˇ»
7265 );
7266 "});
7267 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7268 cx.assert_editor_state(indoc! {"
7269 const a: B = (
7270 c(),
7271 ˇ
7272 );
7273 "});
7274
7275 // Paste it at the same position.
7276 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7277 cx.assert_editor_state(indoc! {"
7278 const a: B = (
7279 c(),
7280 d(
7281 e,
7282 f
7283 )ˇ
7284 );
7285 "});
7286
7287 // Paste it at a line with a lower indent level.
7288 cx.set_state(indoc! {"
7289 ˇ
7290 const a: B = (
7291 c(),
7292 );
7293 "});
7294 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7295 cx.assert_editor_state(indoc! {"
7296 d(
7297 e,
7298 f
7299 )ˇ
7300 const a: B = (
7301 c(),
7302 );
7303 "});
7304
7305 // Cut an indented block, with the leading whitespace.
7306 cx.set_state(indoc! {"
7307 const a: B = (
7308 c(),
7309 « d(
7310 e,
7311 f
7312 )
7313 ˇ»);
7314 "});
7315 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7316 cx.assert_editor_state(indoc! {"
7317 const a: B = (
7318 c(),
7319 ˇ);
7320 "});
7321
7322 // Paste it at the same position.
7323 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7324 cx.assert_editor_state(indoc! {"
7325 const a: B = (
7326 c(),
7327 d(
7328 e,
7329 f
7330 )
7331 ˇ);
7332 "});
7333
7334 // Paste it at a line with a higher indent level.
7335 cx.set_state(indoc! {"
7336 const a: B = (
7337 c(),
7338 d(
7339 e,
7340 fˇ
7341 )
7342 );
7343 "});
7344 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7345 cx.assert_editor_state(indoc! {"
7346 const a: B = (
7347 c(),
7348 d(
7349 e,
7350 f d(
7351 e,
7352 f
7353 )
7354 ˇ
7355 )
7356 );
7357 "});
7358
7359 // Copy an indented block, starting mid-line
7360 cx.set_state(indoc! {"
7361 const a: B = (
7362 c(),
7363 somethin«g(
7364 e,
7365 f
7366 )ˇ»
7367 );
7368 "});
7369 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7370
7371 // Paste it on a line with a lower indent level
7372 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7373 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7374 cx.assert_editor_state(indoc! {"
7375 const a: B = (
7376 c(),
7377 something(
7378 e,
7379 f
7380 )
7381 );
7382 g(
7383 e,
7384 f
7385 )ˇ"});
7386}
7387
7388#[gpui::test]
7389async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7390 init_test(cx, |_| {});
7391
7392 cx.write_to_clipboard(ClipboardItem::new_string(
7393 " d(\n e\n );\n".into(),
7394 ));
7395
7396 let mut cx = EditorTestContext::new(cx).await;
7397 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7398
7399 cx.set_state(indoc! {"
7400 fn a() {
7401 b();
7402 if c() {
7403 ˇ
7404 }
7405 }
7406 "});
7407
7408 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7409 cx.assert_editor_state(indoc! {"
7410 fn a() {
7411 b();
7412 if c() {
7413 d(
7414 e
7415 );
7416 ˇ
7417 }
7418 }
7419 "});
7420
7421 cx.set_state(indoc! {"
7422 fn a() {
7423 b();
7424 ˇ
7425 }
7426 "});
7427
7428 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7429 cx.assert_editor_state(indoc! {"
7430 fn a() {
7431 b();
7432 d(
7433 e
7434 );
7435 ˇ
7436 }
7437 "});
7438}
7439
7440#[gpui::test]
7441fn test_select_all(cx: &mut TestAppContext) {
7442 init_test(cx, |_| {});
7443
7444 let editor = cx.add_window(|window, cx| {
7445 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7446 build_editor(buffer, window, cx)
7447 });
7448 _ = editor.update(cx, |editor, window, cx| {
7449 editor.select_all(&SelectAll, window, cx);
7450 assert_eq!(
7451 editor.selections.display_ranges(cx),
7452 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7453 );
7454 });
7455}
7456
7457#[gpui::test]
7458fn test_select_line(cx: &mut TestAppContext) {
7459 init_test(cx, |_| {});
7460
7461 let editor = cx.add_window(|window, cx| {
7462 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7463 build_editor(buffer, window, cx)
7464 });
7465 _ = editor.update(cx, |editor, window, cx| {
7466 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7467 s.select_display_ranges([
7468 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7469 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7470 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7471 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7472 ])
7473 });
7474 editor.select_line(&SelectLine, window, cx);
7475 assert_eq!(
7476 editor.selections.display_ranges(cx),
7477 vec![
7478 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7479 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7480 ]
7481 );
7482 });
7483
7484 _ = editor.update(cx, |editor, window, cx| {
7485 editor.select_line(&SelectLine, window, cx);
7486 assert_eq!(
7487 editor.selections.display_ranges(cx),
7488 vec![
7489 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7490 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7491 ]
7492 );
7493 });
7494
7495 _ = editor.update(cx, |editor, window, cx| {
7496 editor.select_line(&SelectLine, window, cx);
7497 assert_eq!(
7498 editor.selections.display_ranges(cx),
7499 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7500 );
7501 });
7502}
7503
7504#[gpui::test]
7505async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7506 init_test(cx, |_| {});
7507 let mut cx = EditorTestContext::new(cx).await;
7508
7509 #[track_caller]
7510 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7511 cx.set_state(initial_state);
7512 cx.update_editor(|e, window, cx| {
7513 e.split_selection_into_lines(&Default::default(), window, cx)
7514 });
7515 cx.assert_editor_state(expected_state);
7516 }
7517
7518 // Selection starts and ends at the middle of lines, left-to-right
7519 test(
7520 &mut cx,
7521 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7522 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7523 );
7524 // Same thing, right-to-left
7525 test(
7526 &mut cx,
7527 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7528 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7529 );
7530
7531 // Whole buffer, left-to-right, last line *doesn't* end with newline
7532 test(
7533 &mut cx,
7534 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7535 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7536 );
7537 // Same thing, right-to-left
7538 test(
7539 &mut cx,
7540 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7541 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7542 );
7543
7544 // Whole buffer, left-to-right, last line ends with newline
7545 test(
7546 &mut cx,
7547 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7548 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7549 );
7550 // Same thing, right-to-left
7551 test(
7552 &mut cx,
7553 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7554 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7555 );
7556
7557 // Starts at the end of a line, ends at the start of another
7558 test(
7559 &mut cx,
7560 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7561 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7562 );
7563}
7564
7565#[gpui::test]
7566async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7567 init_test(cx, |_| {});
7568
7569 let editor = cx.add_window(|window, cx| {
7570 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7571 build_editor(buffer, window, cx)
7572 });
7573
7574 // setup
7575 _ = editor.update(cx, |editor, window, cx| {
7576 editor.fold_creases(
7577 vec![
7578 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7579 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7580 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7581 ],
7582 true,
7583 window,
7584 cx,
7585 );
7586 assert_eq!(
7587 editor.display_text(cx),
7588 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7589 );
7590 });
7591
7592 _ = editor.update(cx, |editor, window, cx| {
7593 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7594 s.select_display_ranges([
7595 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7596 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7597 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7598 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7599 ])
7600 });
7601 editor.split_selection_into_lines(&Default::default(), window, cx);
7602 assert_eq!(
7603 editor.display_text(cx),
7604 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7605 );
7606 });
7607 EditorTestContext::for_editor(editor, cx)
7608 .await
7609 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7610
7611 _ = editor.update(cx, |editor, window, cx| {
7612 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7613 s.select_display_ranges([
7614 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7615 ])
7616 });
7617 editor.split_selection_into_lines(&Default::default(), window, cx);
7618 assert_eq!(
7619 editor.display_text(cx),
7620 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7621 );
7622 assert_eq!(
7623 editor.selections.display_ranges(cx),
7624 [
7625 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7626 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7627 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7628 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7629 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7630 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7631 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7632 ]
7633 );
7634 });
7635 EditorTestContext::for_editor(editor, cx)
7636 .await
7637 .assert_editor_state(
7638 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7639 );
7640}
7641
7642#[gpui::test]
7643async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7644 init_test(cx, |_| {});
7645
7646 let mut cx = EditorTestContext::new(cx).await;
7647
7648 cx.set_state(indoc!(
7649 r#"abc
7650 defˇghi
7651
7652 jk
7653 nlmo
7654 "#
7655 ));
7656
7657 cx.update_editor(|editor, window, cx| {
7658 editor.add_selection_above(&Default::default(), window, cx);
7659 });
7660
7661 cx.assert_editor_state(indoc!(
7662 r#"abcˇ
7663 defˇghi
7664
7665 jk
7666 nlmo
7667 "#
7668 ));
7669
7670 cx.update_editor(|editor, window, cx| {
7671 editor.add_selection_above(&Default::default(), window, cx);
7672 });
7673
7674 cx.assert_editor_state(indoc!(
7675 r#"abcˇ
7676 defˇghi
7677
7678 jk
7679 nlmo
7680 "#
7681 ));
7682
7683 cx.update_editor(|editor, window, cx| {
7684 editor.add_selection_below(&Default::default(), window, cx);
7685 });
7686
7687 cx.assert_editor_state(indoc!(
7688 r#"abc
7689 defˇghi
7690
7691 jk
7692 nlmo
7693 "#
7694 ));
7695
7696 cx.update_editor(|editor, window, cx| {
7697 editor.undo_selection(&Default::default(), window, cx);
7698 });
7699
7700 cx.assert_editor_state(indoc!(
7701 r#"abcˇ
7702 defˇghi
7703
7704 jk
7705 nlmo
7706 "#
7707 ));
7708
7709 cx.update_editor(|editor, window, cx| {
7710 editor.redo_selection(&Default::default(), window, cx);
7711 });
7712
7713 cx.assert_editor_state(indoc!(
7714 r#"abc
7715 defˇghi
7716
7717 jk
7718 nlmo
7719 "#
7720 ));
7721
7722 cx.update_editor(|editor, window, cx| {
7723 editor.add_selection_below(&Default::default(), window, cx);
7724 });
7725
7726 cx.assert_editor_state(indoc!(
7727 r#"abc
7728 defˇghi
7729 ˇ
7730 jk
7731 nlmo
7732 "#
7733 ));
7734
7735 cx.update_editor(|editor, window, cx| {
7736 editor.add_selection_below(&Default::default(), window, cx);
7737 });
7738
7739 cx.assert_editor_state(indoc!(
7740 r#"abc
7741 defˇghi
7742 ˇ
7743 jkˇ
7744 nlmo
7745 "#
7746 ));
7747
7748 cx.update_editor(|editor, window, cx| {
7749 editor.add_selection_below(&Default::default(), window, cx);
7750 });
7751
7752 cx.assert_editor_state(indoc!(
7753 r#"abc
7754 defˇghi
7755 ˇ
7756 jkˇ
7757 nlmˇo
7758 "#
7759 ));
7760
7761 cx.update_editor(|editor, window, cx| {
7762 editor.add_selection_below(&Default::default(), window, cx);
7763 });
7764
7765 cx.assert_editor_state(indoc!(
7766 r#"abc
7767 defˇghi
7768 ˇ
7769 jkˇ
7770 nlmˇo
7771 ˇ"#
7772 ));
7773
7774 // change selections
7775 cx.set_state(indoc!(
7776 r#"abc
7777 def«ˇg»hi
7778
7779 jk
7780 nlmo
7781 "#
7782 ));
7783
7784 cx.update_editor(|editor, window, cx| {
7785 editor.add_selection_below(&Default::default(), window, cx);
7786 });
7787
7788 cx.assert_editor_state(indoc!(
7789 r#"abc
7790 def«ˇg»hi
7791
7792 jk
7793 nlm«ˇo»
7794 "#
7795 ));
7796
7797 cx.update_editor(|editor, window, cx| {
7798 editor.add_selection_below(&Default::default(), window, cx);
7799 });
7800
7801 cx.assert_editor_state(indoc!(
7802 r#"abc
7803 def«ˇg»hi
7804
7805 jk
7806 nlm«ˇo»
7807 "#
7808 ));
7809
7810 cx.update_editor(|editor, window, cx| {
7811 editor.add_selection_above(&Default::default(), window, cx);
7812 });
7813
7814 cx.assert_editor_state(indoc!(
7815 r#"abc
7816 def«ˇg»hi
7817
7818 jk
7819 nlmo
7820 "#
7821 ));
7822
7823 cx.update_editor(|editor, window, cx| {
7824 editor.add_selection_above(&Default::default(), window, cx);
7825 });
7826
7827 cx.assert_editor_state(indoc!(
7828 r#"abc
7829 def«ˇg»hi
7830
7831 jk
7832 nlmo
7833 "#
7834 ));
7835
7836 // Change selections again
7837 cx.set_state(indoc!(
7838 r#"a«bc
7839 defgˇ»hi
7840
7841 jk
7842 nlmo
7843 "#
7844 ));
7845
7846 cx.update_editor(|editor, window, cx| {
7847 editor.add_selection_below(&Default::default(), window, cx);
7848 });
7849
7850 cx.assert_editor_state(indoc!(
7851 r#"a«bcˇ»
7852 d«efgˇ»hi
7853
7854 j«kˇ»
7855 nlmo
7856 "#
7857 ));
7858
7859 cx.update_editor(|editor, window, cx| {
7860 editor.add_selection_below(&Default::default(), window, cx);
7861 });
7862 cx.assert_editor_state(indoc!(
7863 r#"a«bcˇ»
7864 d«efgˇ»hi
7865
7866 j«kˇ»
7867 n«lmoˇ»
7868 "#
7869 ));
7870 cx.update_editor(|editor, window, cx| {
7871 editor.add_selection_above(&Default::default(), window, cx);
7872 });
7873
7874 cx.assert_editor_state(indoc!(
7875 r#"a«bcˇ»
7876 d«efgˇ»hi
7877
7878 j«kˇ»
7879 nlmo
7880 "#
7881 ));
7882
7883 // Change selections again
7884 cx.set_state(indoc!(
7885 r#"abc
7886 d«ˇefghi
7887
7888 jk
7889 nlm»o
7890 "#
7891 ));
7892
7893 cx.update_editor(|editor, window, cx| {
7894 editor.add_selection_above(&Default::default(), window, cx);
7895 });
7896
7897 cx.assert_editor_state(indoc!(
7898 r#"a«ˇbc»
7899 d«ˇef»ghi
7900
7901 j«ˇk»
7902 n«ˇlm»o
7903 "#
7904 ));
7905
7906 cx.update_editor(|editor, window, cx| {
7907 editor.add_selection_below(&Default::default(), window, cx);
7908 });
7909
7910 cx.assert_editor_state(indoc!(
7911 r#"abc
7912 d«ˇef»ghi
7913
7914 j«ˇk»
7915 n«ˇlm»o
7916 "#
7917 ));
7918}
7919
7920#[gpui::test]
7921async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
7922 init_test(cx, |_| {});
7923 let mut cx = EditorTestContext::new(cx).await;
7924
7925 cx.set_state(indoc!(
7926 r#"line onˇe
7927 liˇne two
7928 line three
7929 line four"#
7930 ));
7931
7932 cx.update_editor(|editor, window, cx| {
7933 editor.add_selection_below(&Default::default(), window, cx);
7934 });
7935
7936 // test multiple cursors expand in the same direction
7937 cx.assert_editor_state(indoc!(
7938 r#"line onˇe
7939 liˇne twˇo
7940 liˇne three
7941 line four"#
7942 ));
7943
7944 cx.update_editor(|editor, window, cx| {
7945 editor.add_selection_below(&Default::default(), window, cx);
7946 });
7947
7948 cx.update_editor(|editor, window, cx| {
7949 editor.add_selection_below(&Default::default(), window, cx);
7950 });
7951
7952 // test multiple cursors expand below overflow
7953 cx.assert_editor_state(indoc!(
7954 r#"line onˇe
7955 liˇne twˇo
7956 liˇne thˇree
7957 liˇne foˇur"#
7958 ));
7959
7960 cx.update_editor(|editor, window, cx| {
7961 editor.add_selection_above(&Default::default(), window, cx);
7962 });
7963
7964 // test multiple cursors retrieves back correctly
7965 cx.assert_editor_state(indoc!(
7966 r#"line onˇe
7967 liˇne twˇo
7968 liˇne thˇree
7969 line four"#
7970 ));
7971
7972 cx.update_editor(|editor, window, cx| {
7973 editor.add_selection_above(&Default::default(), window, cx);
7974 });
7975
7976 cx.update_editor(|editor, window, cx| {
7977 editor.add_selection_above(&Default::default(), window, cx);
7978 });
7979
7980 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
7981 cx.assert_editor_state(indoc!(
7982 r#"liˇne onˇe
7983 liˇne two
7984 line three
7985 line four"#
7986 ));
7987
7988 cx.update_editor(|editor, window, cx| {
7989 editor.undo_selection(&Default::default(), window, cx);
7990 });
7991
7992 // test undo
7993 cx.assert_editor_state(indoc!(
7994 r#"line onˇe
7995 liˇne twˇo
7996 line three
7997 line four"#
7998 ));
7999
8000 cx.update_editor(|editor, window, cx| {
8001 editor.redo_selection(&Default::default(), window, cx);
8002 });
8003
8004 // test redo
8005 cx.assert_editor_state(indoc!(
8006 r#"liˇne onˇe
8007 liˇne two
8008 line three
8009 line four"#
8010 ));
8011
8012 cx.set_state(indoc!(
8013 r#"abcd
8014 ef«ghˇ»
8015 ijkl
8016 «mˇ»nop"#
8017 ));
8018
8019 cx.update_editor(|editor, window, cx| {
8020 editor.add_selection_above(&Default::default(), window, cx);
8021 });
8022
8023 // test multiple selections expand in the same direction
8024 cx.assert_editor_state(indoc!(
8025 r#"ab«cdˇ»
8026 ef«ghˇ»
8027 «iˇ»jkl
8028 «mˇ»nop"#
8029 ));
8030
8031 cx.update_editor(|editor, window, cx| {
8032 editor.add_selection_above(&Default::default(), window, cx);
8033 });
8034
8035 // test multiple selection upward overflow
8036 cx.assert_editor_state(indoc!(
8037 r#"ab«cdˇ»
8038 «eˇ»f«ghˇ»
8039 «iˇ»jkl
8040 «mˇ»nop"#
8041 ));
8042
8043 cx.update_editor(|editor, window, cx| {
8044 editor.add_selection_below(&Default::default(), window, cx);
8045 });
8046
8047 // test multiple selection retrieves back correctly
8048 cx.assert_editor_state(indoc!(
8049 r#"abcd
8050 ef«ghˇ»
8051 «iˇ»jkl
8052 «mˇ»nop"#
8053 ));
8054
8055 cx.update_editor(|editor, window, cx| {
8056 editor.add_selection_below(&Default::default(), window, cx);
8057 });
8058
8059 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8060 cx.assert_editor_state(indoc!(
8061 r#"abcd
8062 ef«ghˇ»
8063 ij«klˇ»
8064 «mˇ»nop"#
8065 ));
8066
8067 cx.update_editor(|editor, window, cx| {
8068 editor.undo_selection(&Default::default(), window, cx);
8069 });
8070
8071 // test undo
8072 cx.assert_editor_state(indoc!(
8073 r#"abcd
8074 ef«ghˇ»
8075 «iˇ»jkl
8076 «mˇ»nop"#
8077 ));
8078
8079 cx.update_editor(|editor, window, cx| {
8080 editor.redo_selection(&Default::default(), window, cx);
8081 });
8082
8083 // test redo
8084 cx.assert_editor_state(indoc!(
8085 r#"abcd
8086 ef«ghˇ»
8087 ij«klˇ»
8088 «mˇ»nop"#
8089 ));
8090}
8091
8092#[gpui::test]
8093async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8094 init_test(cx, |_| {});
8095 let mut cx = EditorTestContext::new(cx).await;
8096
8097 cx.set_state(indoc!(
8098 r#"line onˇe
8099 liˇne two
8100 line three
8101 line four"#
8102 ));
8103
8104 cx.update_editor(|editor, window, cx| {
8105 editor.add_selection_below(&Default::default(), window, cx);
8106 editor.add_selection_below(&Default::default(), window, cx);
8107 editor.add_selection_below(&Default::default(), window, cx);
8108 });
8109
8110 // initial state with two multi cursor groups
8111 cx.assert_editor_state(indoc!(
8112 r#"line onˇe
8113 liˇne twˇo
8114 liˇne thˇree
8115 liˇne foˇur"#
8116 ));
8117
8118 // add single cursor in middle - simulate opt click
8119 cx.update_editor(|editor, window, cx| {
8120 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8121 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8122 editor.end_selection(window, cx);
8123 });
8124
8125 cx.assert_editor_state(indoc!(
8126 r#"line onˇe
8127 liˇne twˇo
8128 liˇneˇ thˇree
8129 liˇne foˇur"#
8130 ));
8131
8132 cx.update_editor(|editor, window, cx| {
8133 editor.add_selection_above(&Default::default(), window, cx);
8134 });
8135
8136 // test new added selection expands above and existing selection shrinks
8137 cx.assert_editor_state(indoc!(
8138 r#"line onˇe
8139 liˇneˇ twˇo
8140 liˇneˇ thˇree
8141 line four"#
8142 ));
8143
8144 cx.update_editor(|editor, window, cx| {
8145 editor.add_selection_above(&Default::default(), window, cx);
8146 });
8147
8148 // test new added selection expands above and existing selection shrinks
8149 cx.assert_editor_state(indoc!(
8150 r#"lineˇ onˇe
8151 liˇneˇ twˇo
8152 lineˇ three
8153 line four"#
8154 ));
8155
8156 // intial state with two selection groups
8157 cx.set_state(indoc!(
8158 r#"abcd
8159 ef«ghˇ»
8160 ijkl
8161 «mˇ»nop"#
8162 ));
8163
8164 cx.update_editor(|editor, window, cx| {
8165 editor.add_selection_above(&Default::default(), window, cx);
8166 editor.add_selection_above(&Default::default(), window, cx);
8167 });
8168
8169 cx.assert_editor_state(indoc!(
8170 r#"ab«cdˇ»
8171 «eˇ»f«ghˇ»
8172 «iˇ»jkl
8173 «mˇ»nop"#
8174 ));
8175
8176 // add single selection in middle - simulate opt drag
8177 cx.update_editor(|editor, window, cx| {
8178 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8179 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8180 editor.update_selection(
8181 DisplayPoint::new(DisplayRow(2), 4),
8182 0,
8183 gpui::Point::<f32>::default(),
8184 window,
8185 cx,
8186 );
8187 editor.end_selection(window, cx);
8188 });
8189
8190 cx.assert_editor_state(indoc!(
8191 r#"ab«cdˇ»
8192 «eˇ»f«ghˇ»
8193 «iˇ»jk«lˇ»
8194 «mˇ»nop"#
8195 ));
8196
8197 cx.update_editor(|editor, window, cx| {
8198 editor.add_selection_below(&Default::default(), window, cx);
8199 });
8200
8201 // test new added selection expands below, others shrinks from above
8202 cx.assert_editor_state(indoc!(
8203 r#"abcd
8204 ef«ghˇ»
8205 «iˇ»jk«lˇ»
8206 «mˇ»no«pˇ»"#
8207 ));
8208}
8209
8210#[gpui::test]
8211async fn test_select_next(cx: &mut TestAppContext) {
8212 init_test(cx, |_| {});
8213
8214 let mut cx = EditorTestContext::new(cx).await;
8215 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8216
8217 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8218 .unwrap();
8219 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8220
8221 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8222 .unwrap();
8223 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8224
8225 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8226 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8227
8228 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8229 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8230
8231 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8232 .unwrap();
8233 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8234
8235 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8236 .unwrap();
8237 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8238
8239 // Test selection direction should be preserved
8240 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8241
8242 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8243 .unwrap();
8244 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8245}
8246
8247#[gpui::test]
8248async fn test_select_all_matches(cx: &mut TestAppContext) {
8249 init_test(cx, |_| {});
8250
8251 let mut cx = EditorTestContext::new(cx).await;
8252
8253 // Test caret-only selections
8254 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8255 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8256 .unwrap();
8257 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8258
8259 // Test left-to-right selections
8260 cx.set_state("abc\n«abcˇ»\nabc");
8261 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8262 .unwrap();
8263 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8264
8265 // Test right-to-left selections
8266 cx.set_state("abc\n«ˇabc»\nabc");
8267 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8268 .unwrap();
8269 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8270
8271 // Test selecting whitespace with caret selection
8272 cx.set_state("abc\nˇ abc\nabc");
8273 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8274 .unwrap();
8275 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8276
8277 // Test selecting whitespace with left-to-right selection
8278 cx.set_state("abc\n«ˇ »abc\nabc");
8279 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8280 .unwrap();
8281 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8282
8283 // Test no matches with right-to-left selection
8284 cx.set_state("abc\n« ˇ»abc\nabc");
8285 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8286 .unwrap();
8287 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8288
8289 // Test with a single word and clip_at_line_ends=true (#29823)
8290 cx.set_state("aˇbc");
8291 cx.update_editor(|e, window, cx| {
8292 e.set_clip_at_line_ends(true, cx);
8293 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8294 e.set_clip_at_line_ends(false, cx);
8295 });
8296 cx.assert_editor_state("«abcˇ»");
8297}
8298
8299#[gpui::test]
8300async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8301 init_test(cx, |_| {});
8302
8303 let mut cx = EditorTestContext::new(cx).await;
8304
8305 let large_body_1 = "\nd".repeat(200);
8306 let large_body_2 = "\ne".repeat(200);
8307
8308 cx.set_state(&format!(
8309 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8310 ));
8311 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8312 let scroll_position = editor.scroll_position(cx);
8313 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8314 scroll_position
8315 });
8316
8317 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8318 .unwrap();
8319 cx.assert_editor_state(&format!(
8320 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8321 ));
8322 let scroll_position_after_selection =
8323 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8324 assert_eq!(
8325 initial_scroll_position, scroll_position_after_selection,
8326 "Scroll position should not change after selecting all matches"
8327 );
8328}
8329
8330#[gpui::test]
8331async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8332 init_test(cx, |_| {});
8333
8334 let mut cx = EditorLspTestContext::new_rust(
8335 lsp::ServerCapabilities {
8336 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8337 ..Default::default()
8338 },
8339 cx,
8340 )
8341 .await;
8342
8343 cx.set_state(indoc! {"
8344 line 1
8345 line 2
8346 linˇe 3
8347 line 4
8348 line 5
8349 "});
8350
8351 // Make an edit
8352 cx.update_editor(|editor, window, cx| {
8353 editor.handle_input("X", window, cx);
8354 });
8355
8356 // Move cursor to a different position
8357 cx.update_editor(|editor, window, cx| {
8358 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8359 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8360 });
8361 });
8362
8363 cx.assert_editor_state(indoc! {"
8364 line 1
8365 line 2
8366 linXe 3
8367 line 4
8368 liˇne 5
8369 "});
8370
8371 cx.lsp
8372 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8373 Ok(Some(vec![lsp::TextEdit::new(
8374 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8375 "PREFIX ".to_string(),
8376 )]))
8377 });
8378
8379 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8380 .unwrap()
8381 .await
8382 .unwrap();
8383
8384 cx.assert_editor_state(indoc! {"
8385 PREFIX line 1
8386 line 2
8387 linXe 3
8388 line 4
8389 liˇne 5
8390 "});
8391
8392 // Undo formatting
8393 cx.update_editor(|editor, window, cx| {
8394 editor.undo(&Default::default(), window, cx);
8395 });
8396
8397 // Verify cursor moved back to position after edit
8398 cx.assert_editor_state(indoc! {"
8399 line 1
8400 line 2
8401 linXˇe 3
8402 line 4
8403 line 5
8404 "});
8405}
8406
8407#[gpui::test]
8408async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8409 init_test(cx, |_| {});
8410
8411 let mut cx = EditorTestContext::new(cx).await;
8412
8413 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8414 cx.update_editor(|editor, window, cx| {
8415 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8416 });
8417
8418 cx.set_state(indoc! {"
8419 line 1
8420 line 2
8421 linˇe 3
8422 line 4
8423 line 5
8424 line 6
8425 line 7
8426 line 8
8427 line 9
8428 line 10
8429 "});
8430
8431 let snapshot = cx.buffer_snapshot();
8432 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8433
8434 cx.update(|_, cx| {
8435 provider.update(cx, |provider, _| {
8436 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8437 id: None,
8438 edits: vec![(edit_position..edit_position, "X".into())],
8439 edit_preview: None,
8440 }))
8441 })
8442 });
8443
8444 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8445 cx.update_editor(|editor, window, cx| {
8446 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8447 });
8448
8449 cx.assert_editor_state(indoc! {"
8450 line 1
8451 line 2
8452 lineXˇ 3
8453 line 4
8454 line 5
8455 line 6
8456 line 7
8457 line 8
8458 line 9
8459 line 10
8460 "});
8461
8462 cx.update_editor(|editor, window, cx| {
8463 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8464 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8465 });
8466 });
8467
8468 cx.assert_editor_state(indoc! {"
8469 line 1
8470 line 2
8471 lineX 3
8472 line 4
8473 line 5
8474 line 6
8475 line 7
8476 line 8
8477 line 9
8478 liˇne 10
8479 "});
8480
8481 cx.update_editor(|editor, window, cx| {
8482 editor.undo(&Default::default(), window, cx);
8483 });
8484
8485 cx.assert_editor_state(indoc! {"
8486 line 1
8487 line 2
8488 lineˇ 3
8489 line 4
8490 line 5
8491 line 6
8492 line 7
8493 line 8
8494 line 9
8495 line 10
8496 "});
8497}
8498
8499#[gpui::test]
8500async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8501 init_test(cx, |_| {});
8502
8503 let mut cx = EditorTestContext::new(cx).await;
8504 cx.set_state(
8505 r#"let foo = 2;
8506lˇet foo = 2;
8507let fooˇ = 2;
8508let foo = 2;
8509let foo = ˇ2;"#,
8510 );
8511
8512 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8513 .unwrap();
8514 cx.assert_editor_state(
8515 r#"let foo = 2;
8516«letˇ» foo = 2;
8517let «fooˇ» = 2;
8518let foo = 2;
8519let foo = «2ˇ»;"#,
8520 );
8521
8522 // noop for multiple selections with different contents
8523 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8524 .unwrap();
8525 cx.assert_editor_state(
8526 r#"let foo = 2;
8527«letˇ» foo = 2;
8528let «fooˇ» = 2;
8529let foo = 2;
8530let foo = «2ˇ»;"#,
8531 );
8532
8533 // Test last selection direction should be preserved
8534 cx.set_state(
8535 r#"let foo = 2;
8536let foo = 2;
8537let «fooˇ» = 2;
8538let «ˇfoo» = 2;
8539let foo = 2;"#,
8540 );
8541
8542 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8543 .unwrap();
8544 cx.assert_editor_state(
8545 r#"let foo = 2;
8546let foo = 2;
8547let «fooˇ» = 2;
8548let «ˇfoo» = 2;
8549let «ˇfoo» = 2;"#,
8550 );
8551}
8552
8553#[gpui::test]
8554async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8555 init_test(cx, |_| {});
8556
8557 let mut cx =
8558 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8559
8560 cx.assert_editor_state(indoc! {"
8561 ˇbbb
8562 ccc
8563
8564 bbb
8565 ccc
8566 "});
8567 cx.dispatch_action(SelectPrevious::default());
8568 cx.assert_editor_state(indoc! {"
8569 «bbbˇ»
8570 ccc
8571
8572 bbb
8573 ccc
8574 "});
8575 cx.dispatch_action(SelectPrevious::default());
8576 cx.assert_editor_state(indoc! {"
8577 «bbbˇ»
8578 ccc
8579
8580 «bbbˇ»
8581 ccc
8582 "});
8583}
8584
8585#[gpui::test]
8586async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8587 init_test(cx, |_| {});
8588
8589 let mut cx = EditorTestContext::new(cx).await;
8590 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8591
8592 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8593 .unwrap();
8594 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8595
8596 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8597 .unwrap();
8598 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8599
8600 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8601 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8602
8603 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8604 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8605
8606 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8607 .unwrap();
8608 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8609
8610 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8611 .unwrap();
8612 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8613}
8614
8615#[gpui::test]
8616async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8617 init_test(cx, |_| {});
8618
8619 let mut cx = EditorTestContext::new(cx).await;
8620 cx.set_state("aˇ");
8621
8622 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8623 .unwrap();
8624 cx.assert_editor_state("«aˇ»");
8625 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8626 .unwrap();
8627 cx.assert_editor_state("«aˇ»");
8628}
8629
8630#[gpui::test]
8631async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8632 init_test(cx, |_| {});
8633
8634 let mut cx = EditorTestContext::new(cx).await;
8635 cx.set_state(
8636 r#"let foo = 2;
8637lˇet foo = 2;
8638let fooˇ = 2;
8639let foo = 2;
8640let foo = ˇ2;"#,
8641 );
8642
8643 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8644 .unwrap();
8645 cx.assert_editor_state(
8646 r#"let foo = 2;
8647«letˇ» foo = 2;
8648let «fooˇ» = 2;
8649let foo = 2;
8650let foo = «2ˇ»;"#,
8651 );
8652
8653 // noop for multiple selections with different contents
8654 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8655 .unwrap();
8656 cx.assert_editor_state(
8657 r#"let foo = 2;
8658«letˇ» foo = 2;
8659let «fooˇ» = 2;
8660let foo = 2;
8661let foo = «2ˇ»;"#,
8662 );
8663}
8664
8665#[gpui::test]
8666async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8667 init_test(cx, |_| {});
8668
8669 let mut cx = EditorTestContext::new(cx).await;
8670 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8671
8672 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8673 .unwrap();
8674 // selection direction is preserved
8675 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8676
8677 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8678 .unwrap();
8679 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8680
8681 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8682 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8683
8684 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8685 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8686
8687 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8688 .unwrap();
8689 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8690
8691 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8692 .unwrap();
8693 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8694}
8695
8696#[gpui::test]
8697async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8698 init_test(cx, |_| {});
8699
8700 let language = Arc::new(Language::new(
8701 LanguageConfig::default(),
8702 Some(tree_sitter_rust::LANGUAGE.into()),
8703 ));
8704
8705 let text = r#"
8706 use mod1::mod2::{mod3, mod4};
8707
8708 fn fn_1(param1: bool, param2: &str) {
8709 let var1 = "text";
8710 }
8711 "#
8712 .unindent();
8713
8714 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8715 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8716 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8717
8718 editor
8719 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8720 .await;
8721
8722 editor.update_in(cx, |editor, window, cx| {
8723 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8724 s.select_display_ranges([
8725 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8726 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8727 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8728 ]);
8729 });
8730 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8731 });
8732 editor.update(cx, |editor, cx| {
8733 assert_text_with_selections(
8734 editor,
8735 indoc! {r#"
8736 use mod1::mod2::{mod3, «mod4ˇ»};
8737
8738 fn fn_1«ˇ(param1: bool, param2: &str)» {
8739 let var1 = "«ˇtext»";
8740 }
8741 "#},
8742 cx,
8743 );
8744 });
8745
8746 editor.update_in(cx, |editor, window, cx| {
8747 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8748 });
8749 editor.update(cx, |editor, cx| {
8750 assert_text_with_selections(
8751 editor,
8752 indoc! {r#"
8753 use mod1::mod2::«{mod3, mod4}ˇ»;
8754
8755 «ˇfn fn_1(param1: bool, param2: &str) {
8756 let var1 = "text";
8757 }»
8758 "#},
8759 cx,
8760 );
8761 });
8762
8763 editor.update_in(cx, |editor, window, cx| {
8764 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8765 });
8766 assert_eq!(
8767 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8768 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8769 );
8770
8771 // Trying to expand the selected syntax node one more time has no effect.
8772 editor.update_in(cx, |editor, window, cx| {
8773 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8774 });
8775 assert_eq!(
8776 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8777 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8778 );
8779
8780 editor.update_in(cx, |editor, window, cx| {
8781 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8782 });
8783 editor.update(cx, |editor, cx| {
8784 assert_text_with_selections(
8785 editor,
8786 indoc! {r#"
8787 use mod1::mod2::«{mod3, mod4}ˇ»;
8788
8789 «ˇfn fn_1(param1: bool, param2: &str) {
8790 let var1 = "text";
8791 }»
8792 "#},
8793 cx,
8794 );
8795 });
8796
8797 editor.update_in(cx, |editor, window, cx| {
8798 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8799 });
8800 editor.update(cx, |editor, cx| {
8801 assert_text_with_selections(
8802 editor,
8803 indoc! {r#"
8804 use mod1::mod2::{mod3, «mod4ˇ»};
8805
8806 fn fn_1«ˇ(param1: bool, param2: &str)» {
8807 let var1 = "«ˇtext»";
8808 }
8809 "#},
8810 cx,
8811 );
8812 });
8813
8814 editor.update_in(cx, |editor, window, cx| {
8815 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8816 });
8817 editor.update(cx, |editor, cx| {
8818 assert_text_with_selections(
8819 editor,
8820 indoc! {r#"
8821 use mod1::mod2::{mod3, moˇd4};
8822
8823 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8824 let var1 = "teˇxt";
8825 }
8826 "#},
8827 cx,
8828 );
8829 });
8830
8831 // Trying to shrink the selected syntax node one more time has no effect.
8832 editor.update_in(cx, |editor, window, cx| {
8833 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8834 });
8835 editor.update_in(cx, |editor, _, cx| {
8836 assert_text_with_selections(
8837 editor,
8838 indoc! {r#"
8839 use mod1::mod2::{mod3, moˇd4};
8840
8841 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8842 let var1 = "teˇxt";
8843 }
8844 "#},
8845 cx,
8846 );
8847 });
8848
8849 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8850 // a fold.
8851 editor.update_in(cx, |editor, window, cx| {
8852 editor.fold_creases(
8853 vec![
8854 Crease::simple(
8855 Point::new(0, 21)..Point::new(0, 24),
8856 FoldPlaceholder::test(),
8857 ),
8858 Crease::simple(
8859 Point::new(3, 20)..Point::new(3, 22),
8860 FoldPlaceholder::test(),
8861 ),
8862 ],
8863 true,
8864 window,
8865 cx,
8866 );
8867 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8868 });
8869 editor.update(cx, |editor, cx| {
8870 assert_text_with_selections(
8871 editor,
8872 indoc! {r#"
8873 use mod1::mod2::«{mod3, mod4}ˇ»;
8874
8875 fn fn_1«ˇ(param1: bool, param2: &str)» {
8876 let var1 = "«ˇtext»";
8877 }
8878 "#},
8879 cx,
8880 );
8881 });
8882}
8883
8884#[gpui::test]
8885async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8886 init_test(cx, |_| {});
8887
8888 let language = Arc::new(Language::new(
8889 LanguageConfig::default(),
8890 Some(tree_sitter_rust::LANGUAGE.into()),
8891 ));
8892
8893 let text = "let a = 2;";
8894
8895 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8896 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8897 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8898
8899 editor
8900 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8901 .await;
8902
8903 // Test case 1: Cursor at end of word
8904 editor.update_in(cx, |editor, window, cx| {
8905 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8906 s.select_display_ranges([
8907 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
8908 ]);
8909 });
8910 });
8911 editor.update(cx, |editor, cx| {
8912 assert_text_with_selections(editor, "let aˇ = 2;", cx);
8913 });
8914 editor.update_in(cx, |editor, window, cx| {
8915 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8916 });
8917 editor.update(cx, |editor, cx| {
8918 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
8919 });
8920 editor.update_in(cx, |editor, window, cx| {
8921 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8922 });
8923 editor.update(cx, |editor, cx| {
8924 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8925 });
8926
8927 // Test case 2: Cursor at end of statement
8928 editor.update_in(cx, |editor, window, cx| {
8929 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8930 s.select_display_ranges([
8931 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
8932 ]);
8933 });
8934 });
8935 editor.update(cx, |editor, cx| {
8936 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
8937 });
8938 editor.update_in(cx, |editor, window, cx| {
8939 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8940 });
8941 editor.update(cx, |editor, cx| {
8942 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8943 });
8944}
8945
8946#[gpui::test]
8947async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
8948 init_test(cx, |_| {});
8949
8950 let language = Arc::new(Language::new(
8951 LanguageConfig {
8952 name: "JavaScript".into(),
8953 ..Default::default()
8954 },
8955 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8956 ));
8957
8958 let text = r#"
8959 let a = {
8960 key: "value",
8961 };
8962 "#
8963 .unindent();
8964
8965 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8966 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8967 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8968
8969 editor
8970 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8971 .await;
8972
8973 // Test case 1: Cursor after '{'
8974 editor.update_in(cx, |editor, window, cx| {
8975 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8976 s.select_display_ranges([
8977 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
8978 ]);
8979 });
8980 });
8981 editor.update(cx, |editor, cx| {
8982 assert_text_with_selections(
8983 editor,
8984 indoc! {r#"
8985 let a = {ˇ
8986 key: "value",
8987 };
8988 "#},
8989 cx,
8990 );
8991 });
8992 editor.update_in(cx, |editor, window, cx| {
8993 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8994 });
8995 editor.update(cx, |editor, cx| {
8996 assert_text_with_selections(
8997 editor,
8998 indoc! {r#"
8999 let a = «ˇ{
9000 key: "value",
9001 }»;
9002 "#},
9003 cx,
9004 );
9005 });
9006
9007 // Test case 2: Cursor after ':'
9008 editor.update_in(cx, |editor, window, cx| {
9009 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9010 s.select_display_ranges([
9011 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9012 ]);
9013 });
9014 });
9015 editor.update(cx, |editor, cx| {
9016 assert_text_with_selections(
9017 editor,
9018 indoc! {r#"
9019 let a = {
9020 key:ˇ "value",
9021 };
9022 "#},
9023 cx,
9024 );
9025 });
9026 editor.update_in(cx, |editor, window, cx| {
9027 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9028 });
9029 editor.update(cx, |editor, cx| {
9030 assert_text_with_selections(
9031 editor,
9032 indoc! {r#"
9033 let a = {
9034 «ˇkey: "value"»,
9035 };
9036 "#},
9037 cx,
9038 );
9039 });
9040 editor.update_in(cx, |editor, window, cx| {
9041 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9042 });
9043 editor.update(cx, |editor, cx| {
9044 assert_text_with_selections(
9045 editor,
9046 indoc! {r#"
9047 let a = «ˇ{
9048 key: "value",
9049 }»;
9050 "#},
9051 cx,
9052 );
9053 });
9054
9055 // Test case 3: Cursor after ','
9056 editor.update_in(cx, |editor, window, cx| {
9057 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9058 s.select_display_ranges([
9059 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9060 ]);
9061 });
9062 });
9063 editor.update(cx, |editor, cx| {
9064 assert_text_with_selections(
9065 editor,
9066 indoc! {r#"
9067 let a = {
9068 key: "value",ˇ
9069 };
9070 "#},
9071 cx,
9072 );
9073 });
9074 editor.update_in(cx, |editor, window, cx| {
9075 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9076 });
9077 editor.update(cx, |editor, cx| {
9078 assert_text_with_selections(
9079 editor,
9080 indoc! {r#"
9081 let a = «ˇ{
9082 key: "value",
9083 }»;
9084 "#},
9085 cx,
9086 );
9087 });
9088
9089 // Test case 4: Cursor after ';'
9090 editor.update_in(cx, |editor, window, cx| {
9091 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9092 s.select_display_ranges([
9093 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9094 ]);
9095 });
9096 });
9097 editor.update(cx, |editor, cx| {
9098 assert_text_with_selections(
9099 editor,
9100 indoc! {r#"
9101 let a = {
9102 key: "value",
9103 };ˇ
9104 "#},
9105 cx,
9106 );
9107 });
9108 editor.update_in(cx, |editor, window, cx| {
9109 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9110 });
9111 editor.update(cx, |editor, cx| {
9112 assert_text_with_selections(
9113 editor,
9114 indoc! {r#"
9115 «ˇlet a = {
9116 key: "value",
9117 };
9118 »"#},
9119 cx,
9120 );
9121 });
9122}
9123
9124#[gpui::test]
9125async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9126 init_test(cx, |_| {});
9127
9128 let language = Arc::new(Language::new(
9129 LanguageConfig::default(),
9130 Some(tree_sitter_rust::LANGUAGE.into()),
9131 ));
9132
9133 let text = r#"
9134 use mod1::mod2::{mod3, mod4};
9135
9136 fn fn_1(param1: bool, param2: &str) {
9137 let var1 = "hello world";
9138 }
9139 "#
9140 .unindent();
9141
9142 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9143 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9144 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9145
9146 editor
9147 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9148 .await;
9149
9150 // Test 1: Cursor on a letter of a string word
9151 editor.update_in(cx, |editor, window, cx| {
9152 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9153 s.select_display_ranges([
9154 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9155 ]);
9156 });
9157 });
9158 editor.update_in(cx, |editor, window, cx| {
9159 assert_text_with_selections(
9160 editor,
9161 indoc! {r#"
9162 use mod1::mod2::{mod3, mod4};
9163
9164 fn fn_1(param1: bool, param2: &str) {
9165 let var1 = "hˇello world";
9166 }
9167 "#},
9168 cx,
9169 );
9170 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9171 assert_text_with_selections(
9172 editor,
9173 indoc! {r#"
9174 use mod1::mod2::{mod3, mod4};
9175
9176 fn fn_1(param1: bool, param2: &str) {
9177 let var1 = "«ˇhello» world";
9178 }
9179 "#},
9180 cx,
9181 );
9182 });
9183
9184 // Test 2: Partial selection within a word
9185 editor.update_in(cx, |editor, window, cx| {
9186 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9187 s.select_display_ranges([
9188 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9189 ]);
9190 });
9191 });
9192 editor.update_in(cx, |editor, window, cx| {
9193 assert_text_with_selections(
9194 editor,
9195 indoc! {r#"
9196 use mod1::mod2::{mod3, mod4};
9197
9198 fn fn_1(param1: bool, param2: &str) {
9199 let var1 = "h«elˇ»lo world";
9200 }
9201 "#},
9202 cx,
9203 );
9204 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9205 assert_text_with_selections(
9206 editor,
9207 indoc! {r#"
9208 use mod1::mod2::{mod3, mod4};
9209
9210 fn fn_1(param1: bool, param2: &str) {
9211 let var1 = "«ˇhello» world";
9212 }
9213 "#},
9214 cx,
9215 );
9216 });
9217
9218 // Test 3: Complete word already selected
9219 editor.update_in(cx, |editor, window, cx| {
9220 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9221 s.select_display_ranges([
9222 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9223 ]);
9224 });
9225 });
9226 editor.update_in(cx, |editor, window, cx| {
9227 assert_text_with_selections(
9228 editor,
9229 indoc! {r#"
9230 use mod1::mod2::{mod3, mod4};
9231
9232 fn fn_1(param1: bool, param2: &str) {
9233 let var1 = "«helloˇ» world";
9234 }
9235 "#},
9236 cx,
9237 );
9238 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9239 assert_text_with_selections(
9240 editor,
9241 indoc! {r#"
9242 use mod1::mod2::{mod3, mod4};
9243
9244 fn fn_1(param1: bool, param2: &str) {
9245 let var1 = "«hello worldˇ»";
9246 }
9247 "#},
9248 cx,
9249 );
9250 });
9251
9252 // Test 4: Selection spanning across words
9253 editor.update_in(cx, |editor, window, cx| {
9254 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9255 s.select_display_ranges([
9256 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9257 ]);
9258 });
9259 });
9260 editor.update_in(cx, |editor, window, cx| {
9261 assert_text_with_selections(
9262 editor,
9263 indoc! {r#"
9264 use mod1::mod2::{mod3, mod4};
9265
9266 fn fn_1(param1: bool, param2: &str) {
9267 let var1 = "hel«lo woˇ»rld";
9268 }
9269 "#},
9270 cx,
9271 );
9272 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9273 assert_text_with_selections(
9274 editor,
9275 indoc! {r#"
9276 use mod1::mod2::{mod3, mod4};
9277
9278 fn fn_1(param1: bool, param2: &str) {
9279 let var1 = "«ˇhello world»";
9280 }
9281 "#},
9282 cx,
9283 );
9284 });
9285
9286 // Test 5: Expansion beyond string
9287 editor.update_in(cx, |editor, window, cx| {
9288 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9289 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9290 assert_text_with_selections(
9291 editor,
9292 indoc! {r#"
9293 use mod1::mod2::{mod3, mod4};
9294
9295 fn fn_1(param1: bool, param2: &str) {
9296 «ˇlet var1 = "hello world";»
9297 }
9298 "#},
9299 cx,
9300 );
9301 });
9302}
9303
9304#[gpui::test]
9305async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9306 init_test(cx, |_| {});
9307
9308 let mut cx = EditorTestContext::new(cx).await;
9309
9310 let language = Arc::new(Language::new(
9311 LanguageConfig::default(),
9312 Some(tree_sitter_rust::LANGUAGE.into()),
9313 ));
9314
9315 cx.update_buffer(|buffer, cx| {
9316 buffer.set_language(Some(language), cx);
9317 });
9318
9319 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9320 cx.update_editor(|editor, window, cx| {
9321 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9322 });
9323
9324 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9325
9326 cx.set_state(indoc! { r#"fn a() {
9327 // what
9328 // a
9329 // ˇlong
9330 // method
9331 // I
9332 // sure
9333 // hope
9334 // it
9335 // works
9336 }"# });
9337
9338 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9339 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9340 cx.update(|_, cx| {
9341 multi_buffer.update(cx, |multi_buffer, cx| {
9342 multi_buffer.set_excerpts_for_path(
9343 PathKey::for_buffer(&buffer, cx),
9344 buffer,
9345 [Point::new(1, 0)..Point::new(1, 0)],
9346 3,
9347 cx,
9348 );
9349 });
9350 });
9351
9352 let editor2 = cx.new_window_entity(|window, cx| {
9353 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9354 });
9355
9356 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9357 cx.update_editor(|editor, window, cx| {
9358 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9359 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9360 })
9361 });
9362
9363 cx.assert_editor_state(indoc! { "
9364 fn a() {
9365 // what
9366 // a
9367 ˇ // long
9368 // method"});
9369
9370 cx.update_editor(|editor, window, cx| {
9371 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9372 });
9373
9374 // Although we could potentially make the action work when the syntax node
9375 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9376 // did. Maybe we could also expand the excerpt to contain the range?
9377 cx.assert_editor_state(indoc! { "
9378 fn a() {
9379 // what
9380 // a
9381 ˇ // long
9382 // method"});
9383}
9384
9385#[gpui::test]
9386async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9387 init_test(cx, |_| {});
9388
9389 let base_text = r#"
9390 impl A {
9391 // this is an uncommitted comment
9392
9393 fn b() {
9394 c();
9395 }
9396
9397 // this is another uncommitted comment
9398
9399 fn d() {
9400 // e
9401 // f
9402 }
9403 }
9404
9405 fn g() {
9406 // h
9407 }
9408 "#
9409 .unindent();
9410
9411 let text = r#"
9412 ˇimpl A {
9413
9414 fn b() {
9415 c();
9416 }
9417
9418 fn d() {
9419 // e
9420 // f
9421 }
9422 }
9423
9424 fn g() {
9425 // h
9426 }
9427 "#
9428 .unindent();
9429
9430 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9431 cx.set_state(&text);
9432 cx.set_head_text(&base_text);
9433 cx.update_editor(|editor, window, cx| {
9434 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9435 });
9436
9437 cx.assert_state_with_diff(
9438 "
9439 ˇimpl A {
9440 - // this is an uncommitted comment
9441
9442 fn b() {
9443 c();
9444 }
9445
9446 - // this is another uncommitted comment
9447 -
9448 fn d() {
9449 // e
9450 // f
9451 }
9452 }
9453
9454 fn g() {
9455 // h
9456 }
9457 "
9458 .unindent(),
9459 );
9460
9461 let expected_display_text = "
9462 impl A {
9463 // this is an uncommitted comment
9464
9465 fn b() {
9466 ⋯
9467 }
9468
9469 // this is another uncommitted comment
9470
9471 fn d() {
9472 ⋯
9473 }
9474 }
9475
9476 fn g() {
9477 ⋯
9478 }
9479 "
9480 .unindent();
9481
9482 cx.update_editor(|editor, window, cx| {
9483 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9484 assert_eq!(editor.display_text(cx), expected_display_text);
9485 });
9486}
9487
9488#[gpui::test]
9489async fn test_autoindent(cx: &mut TestAppContext) {
9490 init_test(cx, |_| {});
9491
9492 let language = Arc::new(
9493 Language::new(
9494 LanguageConfig {
9495 brackets: BracketPairConfig {
9496 pairs: vec![
9497 BracketPair {
9498 start: "{".to_string(),
9499 end: "}".to_string(),
9500 close: false,
9501 surround: false,
9502 newline: true,
9503 },
9504 BracketPair {
9505 start: "(".to_string(),
9506 end: ")".to_string(),
9507 close: false,
9508 surround: false,
9509 newline: true,
9510 },
9511 ],
9512 ..Default::default()
9513 },
9514 ..Default::default()
9515 },
9516 Some(tree_sitter_rust::LANGUAGE.into()),
9517 )
9518 .with_indents_query(
9519 r#"
9520 (_ "(" ")" @end) @indent
9521 (_ "{" "}" @end) @indent
9522 "#,
9523 )
9524 .unwrap(),
9525 );
9526
9527 let text = "fn a() {}";
9528
9529 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9530 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9531 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9532 editor
9533 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9534 .await;
9535
9536 editor.update_in(cx, |editor, window, cx| {
9537 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9538 s.select_ranges([5..5, 8..8, 9..9])
9539 });
9540 editor.newline(&Newline, window, cx);
9541 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9542 assert_eq!(
9543 editor.selections.ranges(cx),
9544 &[
9545 Point::new(1, 4)..Point::new(1, 4),
9546 Point::new(3, 4)..Point::new(3, 4),
9547 Point::new(5, 0)..Point::new(5, 0)
9548 ]
9549 );
9550 });
9551}
9552
9553#[gpui::test]
9554async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9555 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9556
9557 let language = Arc::new(
9558 Language::new(
9559 LanguageConfig {
9560 brackets: BracketPairConfig {
9561 pairs: vec![
9562 BracketPair {
9563 start: "{".to_string(),
9564 end: "}".to_string(),
9565 close: false,
9566 surround: false,
9567 newline: true,
9568 },
9569 BracketPair {
9570 start: "(".to_string(),
9571 end: ")".to_string(),
9572 close: false,
9573 surround: false,
9574 newline: true,
9575 },
9576 ],
9577 ..Default::default()
9578 },
9579 ..Default::default()
9580 },
9581 Some(tree_sitter_rust::LANGUAGE.into()),
9582 )
9583 .with_indents_query(
9584 r#"
9585 (_ "(" ")" @end) @indent
9586 (_ "{" "}" @end) @indent
9587 "#,
9588 )
9589 .unwrap(),
9590 );
9591
9592 let text = "fn a() {}";
9593
9594 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9595 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9596 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9597 editor
9598 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9599 .await;
9600
9601 editor.update_in(cx, |editor, window, cx| {
9602 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9603 s.select_ranges([5..5, 8..8, 9..9])
9604 });
9605 editor.newline(&Newline, window, cx);
9606 assert_eq!(
9607 editor.text(cx),
9608 indoc!(
9609 "
9610 fn a(
9611
9612 ) {
9613
9614 }
9615 "
9616 )
9617 );
9618 assert_eq!(
9619 editor.selections.ranges(cx),
9620 &[
9621 Point::new(1, 0)..Point::new(1, 0),
9622 Point::new(3, 0)..Point::new(3, 0),
9623 Point::new(5, 0)..Point::new(5, 0)
9624 ]
9625 );
9626 });
9627}
9628
9629#[gpui::test]
9630async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9631 init_test(cx, |settings| {
9632 settings.defaults.auto_indent = Some(true);
9633 settings.languages.0.insert(
9634 "python".into(),
9635 LanguageSettingsContent {
9636 auto_indent: Some(false),
9637 ..Default::default()
9638 },
9639 );
9640 });
9641
9642 let mut cx = EditorTestContext::new(cx).await;
9643
9644 let injected_language = Arc::new(
9645 Language::new(
9646 LanguageConfig {
9647 brackets: BracketPairConfig {
9648 pairs: vec![
9649 BracketPair {
9650 start: "{".to_string(),
9651 end: "}".to_string(),
9652 close: false,
9653 surround: false,
9654 newline: true,
9655 },
9656 BracketPair {
9657 start: "(".to_string(),
9658 end: ")".to_string(),
9659 close: true,
9660 surround: false,
9661 newline: true,
9662 },
9663 ],
9664 ..Default::default()
9665 },
9666 name: "python".into(),
9667 ..Default::default()
9668 },
9669 Some(tree_sitter_python::LANGUAGE.into()),
9670 )
9671 .with_indents_query(
9672 r#"
9673 (_ "(" ")" @end) @indent
9674 (_ "{" "}" @end) @indent
9675 "#,
9676 )
9677 .unwrap(),
9678 );
9679
9680 let language = Arc::new(
9681 Language::new(
9682 LanguageConfig {
9683 brackets: BracketPairConfig {
9684 pairs: vec![
9685 BracketPair {
9686 start: "{".to_string(),
9687 end: "}".to_string(),
9688 close: false,
9689 surround: false,
9690 newline: true,
9691 },
9692 BracketPair {
9693 start: "(".to_string(),
9694 end: ")".to_string(),
9695 close: true,
9696 surround: false,
9697 newline: true,
9698 },
9699 ],
9700 ..Default::default()
9701 },
9702 name: LanguageName::new("rust"),
9703 ..Default::default()
9704 },
9705 Some(tree_sitter_rust::LANGUAGE.into()),
9706 )
9707 .with_indents_query(
9708 r#"
9709 (_ "(" ")" @end) @indent
9710 (_ "{" "}" @end) @indent
9711 "#,
9712 )
9713 .unwrap()
9714 .with_injection_query(
9715 r#"
9716 (macro_invocation
9717 macro: (identifier) @_macro_name
9718 (token_tree) @injection.content
9719 (#set! injection.language "python"))
9720 "#,
9721 )
9722 .unwrap(),
9723 );
9724
9725 cx.language_registry().add(injected_language);
9726 cx.language_registry().add(language.clone());
9727
9728 cx.update_buffer(|buffer, cx| {
9729 buffer.set_language(Some(language), cx);
9730 });
9731
9732 cx.set_state(r#"struct A {ˇ}"#);
9733
9734 cx.update_editor(|editor, window, cx| {
9735 editor.newline(&Default::default(), window, cx);
9736 });
9737
9738 cx.assert_editor_state(indoc!(
9739 "struct A {
9740 ˇ
9741 }"
9742 ));
9743
9744 cx.set_state(r#"select_biased!(ˇ)"#);
9745
9746 cx.update_editor(|editor, window, cx| {
9747 editor.newline(&Default::default(), window, cx);
9748 editor.handle_input("def ", window, cx);
9749 editor.handle_input("(", window, cx);
9750 editor.newline(&Default::default(), window, cx);
9751 editor.handle_input("a", window, cx);
9752 });
9753
9754 cx.assert_editor_state(indoc!(
9755 "select_biased!(
9756 def (
9757 aˇ
9758 )
9759 )"
9760 ));
9761}
9762
9763#[gpui::test]
9764async fn test_autoindent_selections(cx: &mut TestAppContext) {
9765 init_test(cx, |_| {});
9766
9767 {
9768 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9769 cx.set_state(indoc! {"
9770 impl A {
9771
9772 fn b() {}
9773
9774 «fn c() {
9775
9776 }ˇ»
9777 }
9778 "});
9779
9780 cx.update_editor(|editor, window, cx| {
9781 editor.autoindent(&Default::default(), window, cx);
9782 });
9783
9784 cx.assert_editor_state(indoc! {"
9785 impl A {
9786
9787 fn b() {}
9788
9789 «fn c() {
9790
9791 }ˇ»
9792 }
9793 "});
9794 }
9795
9796 {
9797 let mut cx = EditorTestContext::new_multibuffer(
9798 cx,
9799 [indoc! { "
9800 impl A {
9801 «
9802 // a
9803 fn b(){}
9804 »
9805 «
9806 }
9807 fn c(){}
9808 »
9809 "}],
9810 );
9811
9812 let buffer = cx.update_editor(|editor, _, cx| {
9813 let buffer = editor.buffer().update(cx, |buffer, _| {
9814 buffer.all_buffers().iter().next().unwrap().clone()
9815 });
9816 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9817 buffer
9818 });
9819
9820 cx.run_until_parked();
9821 cx.update_editor(|editor, window, cx| {
9822 editor.select_all(&Default::default(), window, cx);
9823 editor.autoindent(&Default::default(), window, cx)
9824 });
9825 cx.run_until_parked();
9826
9827 cx.update(|_, cx| {
9828 assert_eq!(
9829 buffer.read(cx).text(),
9830 indoc! { "
9831 impl A {
9832
9833 // a
9834 fn b(){}
9835
9836
9837 }
9838 fn c(){}
9839
9840 " }
9841 )
9842 });
9843 }
9844}
9845
9846#[gpui::test]
9847async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9848 init_test(cx, |_| {});
9849
9850 let mut cx = EditorTestContext::new(cx).await;
9851
9852 let language = Arc::new(Language::new(
9853 LanguageConfig {
9854 brackets: BracketPairConfig {
9855 pairs: vec![
9856 BracketPair {
9857 start: "{".to_string(),
9858 end: "}".to_string(),
9859 close: true,
9860 surround: true,
9861 newline: true,
9862 },
9863 BracketPair {
9864 start: "(".to_string(),
9865 end: ")".to_string(),
9866 close: true,
9867 surround: true,
9868 newline: true,
9869 },
9870 BracketPair {
9871 start: "/*".to_string(),
9872 end: " */".to_string(),
9873 close: true,
9874 surround: true,
9875 newline: true,
9876 },
9877 BracketPair {
9878 start: "[".to_string(),
9879 end: "]".to_string(),
9880 close: false,
9881 surround: false,
9882 newline: true,
9883 },
9884 BracketPair {
9885 start: "\"".to_string(),
9886 end: "\"".to_string(),
9887 close: true,
9888 surround: true,
9889 newline: false,
9890 },
9891 BracketPair {
9892 start: "<".to_string(),
9893 end: ">".to_string(),
9894 close: false,
9895 surround: true,
9896 newline: true,
9897 },
9898 ],
9899 ..Default::default()
9900 },
9901 autoclose_before: "})]".to_string(),
9902 ..Default::default()
9903 },
9904 Some(tree_sitter_rust::LANGUAGE.into()),
9905 ));
9906
9907 cx.language_registry().add(language.clone());
9908 cx.update_buffer(|buffer, cx| {
9909 buffer.set_language(Some(language), cx);
9910 });
9911
9912 cx.set_state(
9913 &r#"
9914 🏀ˇ
9915 εˇ
9916 ❤️ˇ
9917 "#
9918 .unindent(),
9919 );
9920
9921 // autoclose multiple nested brackets at multiple cursors
9922 cx.update_editor(|editor, window, cx| {
9923 editor.handle_input("{", window, cx);
9924 editor.handle_input("{", window, cx);
9925 editor.handle_input("{", window, cx);
9926 });
9927 cx.assert_editor_state(
9928 &"
9929 🏀{{{ˇ}}}
9930 ε{{{ˇ}}}
9931 ❤️{{{ˇ}}}
9932 "
9933 .unindent(),
9934 );
9935
9936 // insert a different closing bracket
9937 cx.update_editor(|editor, window, cx| {
9938 editor.handle_input(")", window, cx);
9939 });
9940 cx.assert_editor_state(
9941 &"
9942 🏀{{{)ˇ}}}
9943 ε{{{)ˇ}}}
9944 ❤️{{{)ˇ}}}
9945 "
9946 .unindent(),
9947 );
9948
9949 // skip over the auto-closed brackets when typing a closing bracket
9950 cx.update_editor(|editor, window, cx| {
9951 editor.move_right(&MoveRight, window, cx);
9952 editor.handle_input("}", window, cx);
9953 editor.handle_input("}", window, cx);
9954 editor.handle_input("}", window, cx);
9955 });
9956 cx.assert_editor_state(
9957 &"
9958 🏀{{{)}}}}ˇ
9959 ε{{{)}}}}ˇ
9960 ❤️{{{)}}}}ˇ
9961 "
9962 .unindent(),
9963 );
9964
9965 // autoclose multi-character pairs
9966 cx.set_state(
9967 &"
9968 ˇ
9969 ˇ
9970 "
9971 .unindent(),
9972 );
9973 cx.update_editor(|editor, window, cx| {
9974 editor.handle_input("/", window, cx);
9975 editor.handle_input("*", window, cx);
9976 });
9977 cx.assert_editor_state(
9978 &"
9979 /*ˇ */
9980 /*ˇ */
9981 "
9982 .unindent(),
9983 );
9984
9985 // one cursor autocloses a multi-character pair, one cursor
9986 // does not autoclose.
9987 cx.set_state(
9988 &"
9989 /ˇ
9990 ˇ
9991 "
9992 .unindent(),
9993 );
9994 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
9995 cx.assert_editor_state(
9996 &"
9997 /*ˇ */
9998 *ˇ
9999 "
10000 .unindent(),
10001 );
10002
10003 // Don't autoclose if the next character isn't whitespace and isn't
10004 // listed in the language's "autoclose_before" section.
10005 cx.set_state("ˇa b");
10006 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10007 cx.assert_editor_state("{ˇa b");
10008
10009 // Don't autoclose if `close` is false for the bracket pair
10010 cx.set_state("ˇ");
10011 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10012 cx.assert_editor_state("[ˇ");
10013
10014 // Surround with brackets if text is selected
10015 cx.set_state("«aˇ» b");
10016 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10017 cx.assert_editor_state("{«aˇ»} b");
10018
10019 // Autoclose when not immediately after a word character
10020 cx.set_state("a ˇ");
10021 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10022 cx.assert_editor_state("a \"ˇ\"");
10023
10024 // Autoclose pair where the start and end characters are the same
10025 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10026 cx.assert_editor_state("a \"\"ˇ");
10027
10028 // Don't autoclose when immediately after a word character
10029 cx.set_state("aˇ");
10030 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10031 cx.assert_editor_state("a\"ˇ");
10032
10033 // Do autoclose when after a non-word character
10034 cx.set_state("{ˇ");
10035 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10036 cx.assert_editor_state("{\"ˇ\"");
10037
10038 // Non identical pairs autoclose regardless of preceding character
10039 cx.set_state("aˇ");
10040 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10041 cx.assert_editor_state("a{ˇ}");
10042
10043 // Don't autoclose pair if autoclose is disabled
10044 cx.set_state("ˇ");
10045 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10046 cx.assert_editor_state("<ˇ");
10047
10048 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10049 cx.set_state("«aˇ» b");
10050 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10051 cx.assert_editor_state("<«aˇ»> b");
10052}
10053
10054#[gpui::test]
10055async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10056 init_test(cx, |settings| {
10057 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10058 });
10059
10060 let mut cx = EditorTestContext::new(cx).await;
10061
10062 let language = Arc::new(Language::new(
10063 LanguageConfig {
10064 brackets: BracketPairConfig {
10065 pairs: vec![
10066 BracketPair {
10067 start: "{".to_string(),
10068 end: "}".to_string(),
10069 close: true,
10070 surround: true,
10071 newline: true,
10072 },
10073 BracketPair {
10074 start: "(".to_string(),
10075 end: ")".to_string(),
10076 close: true,
10077 surround: true,
10078 newline: true,
10079 },
10080 BracketPair {
10081 start: "[".to_string(),
10082 end: "]".to_string(),
10083 close: false,
10084 surround: false,
10085 newline: true,
10086 },
10087 ],
10088 ..Default::default()
10089 },
10090 autoclose_before: "})]".to_string(),
10091 ..Default::default()
10092 },
10093 Some(tree_sitter_rust::LANGUAGE.into()),
10094 ));
10095
10096 cx.language_registry().add(language.clone());
10097 cx.update_buffer(|buffer, cx| {
10098 buffer.set_language(Some(language), cx);
10099 });
10100
10101 cx.set_state(
10102 &"
10103 ˇ
10104 ˇ
10105 ˇ
10106 "
10107 .unindent(),
10108 );
10109
10110 // ensure only matching closing brackets are skipped over
10111 cx.update_editor(|editor, window, cx| {
10112 editor.handle_input("}", window, cx);
10113 editor.move_left(&MoveLeft, window, cx);
10114 editor.handle_input(")", window, cx);
10115 editor.move_left(&MoveLeft, window, cx);
10116 });
10117 cx.assert_editor_state(
10118 &"
10119 ˇ)}
10120 ˇ)}
10121 ˇ)}
10122 "
10123 .unindent(),
10124 );
10125
10126 // skip-over closing brackets at multiple cursors
10127 cx.update_editor(|editor, window, cx| {
10128 editor.handle_input(")", window, cx);
10129 editor.handle_input("}", window, cx);
10130 });
10131 cx.assert_editor_state(
10132 &"
10133 )}ˇ
10134 )}ˇ
10135 )}ˇ
10136 "
10137 .unindent(),
10138 );
10139
10140 // ignore non-close brackets
10141 cx.update_editor(|editor, window, cx| {
10142 editor.handle_input("]", window, cx);
10143 editor.move_left(&MoveLeft, window, cx);
10144 editor.handle_input("]", window, cx);
10145 });
10146 cx.assert_editor_state(
10147 &"
10148 )}]ˇ]
10149 )}]ˇ]
10150 )}]ˇ]
10151 "
10152 .unindent(),
10153 );
10154}
10155
10156#[gpui::test]
10157async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10158 init_test(cx, |_| {});
10159
10160 let mut cx = EditorTestContext::new(cx).await;
10161
10162 let html_language = Arc::new(
10163 Language::new(
10164 LanguageConfig {
10165 name: "HTML".into(),
10166 brackets: BracketPairConfig {
10167 pairs: vec![
10168 BracketPair {
10169 start: "<".into(),
10170 end: ">".into(),
10171 close: true,
10172 ..Default::default()
10173 },
10174 BracketPair {
10175 start: "{".into(),
10176 end: "}".into(),
10177 close: true,
10178 ..Default::default()
10179 },
10180 BracketPair {
10181 start: "(".into(),
10182 end: ")".into(),
10183 close: true,
10184 ..Default::default()
10185 },
10186 ],
10187 ..Default::default()
10188 },
10189 autoclose_before: "})]>".into(),
10190 ..Default::default()
10191 },
10192 Some(tree_sitter_html::LANGUAGE.into()),
10193 )
10194 .with_injection_query(
10195 r#"
10196 (script_element
10197 (raw_text) @injection.content
10198 (#set! injection.language "javascript"))
10199 "#,
10200 )
10201 .unwrap(),
10202 );
10203
10204 let javascript_language = Arc::new(Language::new(
10205 LanguageConfig {
10206 name: "JavaScript".into(),
10207 brackets: BracketPairConfig {
10208 pairs: vec![
10209 BracketPair {
10210 start: "/*".into(),
10211 end: " */".into(),
10212 close: true,
10213 ..Default::default()
10214 },
10215 BracketPair {
10216 start: "{".into(),
10217 end: "}".into(),
10218 close: true,
10219 ..Default::default()
10220 },
10221 BracketPair {
10222 start: "(".into(),
10223 end: ")".into(),
10224 close: true,
10225 ..Default::default()
10226 },
10227 ],
10228 ..Default::default()
10229 },
10230 autoclose_before: "})]>".into(),
10231 ..Default::default()
10232 },
10233 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10234 ));
10235
10236 cx.language_registry().add(html_language.clone());
10237 cx.language_registry().add(javascript_language);
10238 cx.executor().run_until_parked();
10239
10240 cx.update_buffer(|buffer, cx| {
10241 buffer.set_language(Some(html_language), cx);
10242 });
10243
10244 cx.set_state(
10245 &r#"
10246 <body>ˇ
10247 <script>
10248 var x = 1;ˇ
10249 </script>
10250 </body>ˇ
10251 "#
10252 .unindent(),
10253 );
10254
10255 // Precondition: different languages are active at different locations.
10256 cx.update_editor(|editor, window, cx| {
10257 let snapshot = editor.snapshot(window, cx);
10258 let cursors = editor.selections.ranges::<usize>(cx);
10259 let languages = cursors
10260 .iter()
10261 .map(|c| snapshot.language_at(c.start).unwrap().name())
10262 .collect::<Vec<_>>();
10263 assert_eq!(
10264 languages,
10265 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10266 );
10267 });
10268
10269 // Angle brackets autoclose in HTML, but not JavaScript.
10270 cx.update_editor(|editor, window, cx| {
10271 editor.handle_input("<", window, cx);
10272 editor.handle_input("a", window, cx);
10273 });
10274 cx.assert_editor_state(
10275 &r#"
10276 <body><aˇ>
10277 <script>
10278 var x = 1;<aˇ
10279 </script>
10280 </body><aˇ>
10281 "#
10282 .unindent(),
10283 );
10284
10285 // Curly braces and parens autoclose in both HTML and JavaScript.
10286 cx.update_editor(|editor, window, cx| {
10287 editor.handle_input(" b=", window, cx);
10288 editor.handle_input("{", window, cx);
10289 editor.handle_input("c", window, cx);
10290 editor.handle_input("(", window, cx);
10291 });
10292 cx.assert_editor_state(
10293 &r#"
10294 <body><a b={c(ˇ)}>
10295 <script>
10296 var x = 1;<a b={c(ˇ)}
10297 </script>
10298 </body><a b={c(ˇ)}>
10299 "#
10300 .unindent(),
10301 );
10302
10303 // Brackets that were already autoclosed are skipped.
10304 cx.update_editor(|editor, window, cx| {
10305 editor.handle_input(")", window, cx);
10306 editor.handle_input("d", window, cx);
10307 editor.handle_input("}", window, cx);
10308 });
10309 cx.assert_editor_state(
10310 &r#"
10311 <body><a b={c()d}ˇ>
10312 <script>
10313 var x = 1;<a b={c()d}ˇ
10314 </script>
10315 </body><a b={c()d}ˇ>
10316 "#
10317 .unindent(),
10318 );
10319 cx.update_editor(|editor, window, cx| {
10320 editor.handle_input(">", window, cx);
10321 });
10322 cx.assert_editor_state(
10323 &r#"
10324 <body><a b={c()d}>ˇ
10325 <script>
10326 var x = 1;<a b={c()d}>ˇ
10327 </script>
10328 </body><a b={c()d}>ˇ
10329 "#
10330 .unindent(),
10331 );
10332
10333 // Reset
10334 cx.set_state(
10335 &r#"
10336 <body>ˇ
10337 <script>
10338 var x = 1;ˇ
10339 </script>
10340 </body>ˇ
10341 "#
10342 .unindent(),
10343 );
10344
10345 cx.update_editor(|editor, window, cx| {
10346 editor.handle_input("<", window, cx);
10347 });
10348 cx.assert_editor_state(
10349 &r#"
10350 <body><ˇ>
10351 <script>
10352 var x = 1;<ˇ
10353 </script>
10354 </body><ˇ>
10355 "#
10356 .unindent(),
10357 );
10358
10359 // When backspacing, the closing angle brackets are removed.
10360 cx.update_editor(|editor, window, cx| {
10361 editor.backspace(&Backspace, window, cx);
10362 });
10363 cx.assert_editor_state(
10364 &r#"
10365 <body>ˇ
10366 <script>
10367 var x = 1;ˇ
10368 </script>
10369 </body>ˇ
10370 "#
10371 .unindent(),
10372 );
10373
10374 // Block comments autoclose in JavaScript, but not HTML.
10375 cx.update_editor(|editor, window, cx| {
10376 editor.handle_input("/", window, cx);
10377 editor.handle_input("*", window, cx);
10378 });
10379 cx.assert_editor_state(
10380 &r#"
10381 <body>/*ˇ
10382 <script>
10383 var x = 1;/*ˇ */
10384 </script>
10385 </body>/*ˇ
10386 "#
10387 .unindent(),
10388 );
10389}
10390
10391#[gpui::test]
10392async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10393 init_test(cx, |_| {});
10394
10395 let mut cx = EditorTestContext::new(cx).await;
10396
10397 let rust_language = Arc::new(
10398 Language::new(
10399 LanguageConfig {
10400 name: "Rust".into(),
10401 brackets: serde_json::from_value(json!([
10402 { "start": "{", "end": "}", "close": true, "newline": true },
10403 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10404 ]))
10405 .unwrap(),
10406 autoclose_before: "})]>".into(),
10407 ..Default::default()
10408 },
10409 Some(tree_sitter_rust::LANGUAGE.into()),
10410 )
10411 .with_override_query("(string_literal) @string")
10412 .unwrap(),
10413 );
10414
10415 cx.language_registry().add(rust_language.clone());
10416 cx.update_buffer(|buffer, cx| {
10417 buffer.set_language(Some(rust_language), cx);
10418 });
10419
10420 cx.set_state(
10421 &r#"
10422 let x = ˇ
10423 "#
10424 .unindent(),
10425 );
10426
10427 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10428 cx.update_editor(|editor, window, cx| {
10429 editor.handle_input("\"", window, cx);
10430 });
10431 cx.assert_editor_state(
10432 &r#"
10433 let x = "ˇ"
10434 "#
10435 .unindent(),
10436 );
10437
10438 // Inserting another quotation mark. The cursor moves across the existing
10439 // automatically-inserted quotation mark.
10440 cx.update_editor(|editor, window, cx| {
10441 editor.handle_input("\"", window, cx);
10442 });
10443 cx.assert_editor_state(
10444 &r#"
10445 let x = ""ˇ
10446 "#
10447 .unindent(),
10448 );
10449
10450 // Reset
10451 cx.set_state(
10452 &r#"
10453 let x = ˇ
10454 "#
10455 .unindent(),
10456 );
10457
10458 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10459 cx.update_editor(|editor, window, cx| {
10460 editor.handle_input("\"", window, cx);
10461 editor.handle_input(" ", window, cx);
10462 editor.move_left(&Default::default(), window, cx);
10463 editor.handle_input("\\", window, cx);
10464 editor.handle_input("\"", window, cx);
10465 });
10466 cx.assert_editor_state(
10467 &r#"
10468 let x = "\"ˇ "
10469 "#
10470 .unindent(),
10471 );
10472
10473 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10474 // mark. Nothing is inserted.
10475 cx.update_editor(|editor, window, cx| {
10476 editor.move_right(&Default::default(), window, cx);
10477 editor.handle_input("\"", window, cx);
10478 });
10479 cx.assert_editor_state(
10480 &r#"
10481 let x = "\" "ˇ
10482 "#
10483 .unindent(),
10484 );
10485}
10486
10487#[gpui::test]
10488async fn test_surround_with_pair(cx: &mut TestAppContext) {
10489 init_test(cx, |_| {});
10490
10491 let language = Arc::new(Language::new(
10492 LanguageConfig {
10493 brackets: BracketPairConfig {
10494 pairs: vec![
10495 BracketPair {
10496 start: "{".to_string(),
10497 end: "}".to_string(),
10498 close: true,
10499 surround: true,
10500 newline: true,
10501 },
10502 BracketPair {
10503 start: "/* ".to_string(),
10504 end: "*/".to_string(),
10505 close: true,
10506 surround: true,
10507 ..Default::default()
10508 },
10509 ],
10510 ..Default::default()
10511 },
10512 ..Default::default()
10513 },
10514 Some(tree_sitter_rust::LANGUAGE.into()),
10515 ));
10516
10517 let text = r#"
10518 a
10519 b
10520 c
10521 "#
10522 .unindent();
10523
10524 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10525 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10526 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10527 editor
10528 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10529 .await;
10530
10531 editor.update_in(cx, |editor, window, cx| {
10532 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10533 s.select_display_ranges([
10534 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10535 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10536 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10537 ])
10538 });
10539
10540 editor.handle_input("{", window, cx);
10541 editor.handle_input("{", window, cx);
10542 editor.handle_input("{", window, cx);
10543 assert_eq!(
10544 editor.text(cx),
10545 "
10546 {{{a}}}
10547 {{{b}}}
10548 {{{c}}}
10549 "
10550 .unindent()
10551 );
10552 assert_eq!(
10553 editor.selections.display_ranges(cx),
10554 [
10555 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10556 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10557 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10558 ]
10559 );
10560
10561 editor.undo(&Undo, window, cx);
10562 editor.undo(&Undo, window, cx);
10563 editor.undo(&Undo, window, cx);
10564 assert_eq!(
10565 editor.text(cx),
10566 "
10567 a
10568 b
10569 c
10570 "
10571 .unindent()
10572 );
10573 assert_eq!(
10574 editor.selections.display_ranges(cx),
10575 [
10576 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10577 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10578 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10579 ]
10580 );
10581
10582 // Ensure inserting the first character of a multi-byte bracket pair
10583 // doesn't surround the selections with the bracket.
10584 editor.handle_input("/", window, cx);
10585 assert_eq!(
10586 editor.text(cx),
10587 "
10588 /
10589 /
10590 /
10591 "
10592 .unindent()
10593 );
10594 assert_eq!(
10595 editor.selections.display_ranges(cx),
10596 [
10597 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10598 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10599 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10600 ]
10601 );
10602
10603 editor.undo(&Undo, window, cx);
10604 assert_eq!(
10605 editor.text(cx),
10606 "
10607 a
10608 b
10609 c
10610 "
10611 .unindent()
10612 );
10613 assert_eq!(
10614 editor.selections.display_ranges(cx),
10615 [
10616 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10617 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10618 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10619 ]
10620 );
10621
10622 // Ensure inserting the last character of a multi-byte bracket pair
10623 // doesn't surround the selections with the bracket.
10624 editor.handle_input("*", window, cx);
10625 assert_eq!(
10626 editor.text(cx),
10627 "
10628 *
10629 *
10630 *
10631 "
10632 .unindent()
10633 );
10634 assert_eq!(
10635 editor.selections.display_ranges(cx),
10636 [
10637 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10638 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10639 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10640 ]
10641 );
10642 });
10643}
10644
10645#[gpui::test]
10646async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10647 init_test(cx, |_| {});
10648
10649 let language = Arc::new(Language::new(
10650 LanguageConfig {
10651 brackets: BracketPairConfig {
10652 pairs: vec![BracketPair {
10653 start: "{".to_string(),
10654 end: "}".to_string(),
10655 close: true,
10656 surround: true,
10657 newline: true,
10658 }],
10659 ..Default::default()
10660 },
10661 autoclose_before: "}".to_string(),
10662 ..Default::default()
10663 },
10664 Some(tree_sitter_rust::LANGUAGE.into()),
10665 ));
10666
10667 let text = r#"
10668 a
10669 b
10670 c
10671 "#
10672 .unindent();
10673
10674 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10675 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10676 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10677 editor
10678 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10679 .await;
10680
10681 editor.update_in(cx, |editor, window, cx| {
10682 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10683 s.select_ranges([
10684 Point::new(0, 1)..Point::new(0, 1),
10685 Point::new(1, 1)..Point::new(1, 1),
10686 Point::new(2, 1)..Point::new(2, 1),
10687 ])
10688 });
10689
10690 editor.handle_input("{", window, cx);
10691 editor.handle_input("{", window, cx);
10692 editor.handle_input("_", window, cx);
10693 assert_eq!(
10694 editor.text(cx),
10695 "
10696 a{{_}}
10697 b{{_}}
10698 c{{_}}
10699 "
10700 .unindent()
10701 );
10702 assert_eq!(
10703 editor.selections.ranges::<Point>(cx),
10704 [
10705 Point::new(0, 4)..Point::new(0, 4),
10706 Point::new(1, 4)..Point::new(1, 4),
10707 Point::new(2, 4)..Point::new(2, 4)
10708 ]
10709 );
10710
10711 editor.backspace(&Default::default(), window, cx);
10712 editor.backspace(&Default::default(), window, cx);
10713 assert_eq!(
10714 editor.text(cx),
10715 "
10716 a{}
10717 b{}
10718 c{}
10719 "
10720 .unindent()
10721 );
10722 assert_eq!(
10723 editor.selections.ranges::<Point>(cx),
10724 [
10725 Point::new(0, 2)..Point::new(0, 2),
10726 Point::new(1, 2)..Point::new(1, 2),
10727 Point::new(2, 2)..Point::new(2, 2)
10728 ]
10729 );
10730
10731 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10732 assert_eq!(
10733 editor.text(cx),
10734 "
10735 a
10736 b
10737 c
10738 "
10739 .unindent()
10740 );
10741 assert_eq!(
10742 editor.selections.ranges::<Point>(cx),
10743 [
10744 Point::new(0, 1)..Point::new(0, 1),
10745 Point::new(1, 1)..Point::new(1, 1),
10746 Point::new(2, 1)..Point::new(2, 1)
10747 ]
10748 );
10749 });
10750}
10751
10752#[gpui::test]
10753async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10754 init_test(cx, |settings| {
10755 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10756 });
10757
10758 let mut cx = EditorTestContext::new(cx).await;
10759
10760 let language = Arc::new(Language::new(
10761 LanguageConfig {
10762 brackets: BracketPairConfig {
10763 pairs: vec![
10764 BracketPair {
10765 start: "{".to_string(),
10766 end: "}".to_string(),
10767 close: true,
10768 surround: true,
10769 newline: true,
10770 },
10771 BracketPair {
10772 start: "(".to_string(),
10773 end: ")".to_string(),
10774 close: true,
10775 surround: true,
10776 newline: true,
10777 },
10778 BracketPair {
10779 start: "[".to_string(),
10780 end: "]".to_string(),
10781 close: false,
10782 surround: true,
10783 newline: true,
10784 },
10785 ],
10786 ..Default::default()
10787 },
10788 autoclose_before: "})]".to_string(),
10789 ..Default::default()
10790 },
10791 Some(tree_sitter_rust::LANGUAGE.into()),
10792 ));
10793
10794 cx.language_registry().add(language.clone());
10795 cx.update_buffer(|buffer, cx| {
10796 buffer.set_language(Some(language), cx);
10797 });
10798
10799 cx.set_state(
10800 &"
10801 {(ˇ)}
10802 [[ˇ]]
10803 {(ˇ)}
10804 "
10805 .unindent(),
10806 );
10807
10808 cx.update_editor(|editor, window, cx| {
10809 editor.backspace(&Default::default(), window, cx);
10810 editor.backspace(&Default::default(), window, cx);
10811 });
10812
10813 cx.assert_editor_state(
10814 &"
10815 ˇ
10816 ˇ]]
10817 ˇ
10818 "
10819 .unindent(),
10820 );
10821
10822 cx.update_editor(|editor, window, cx| {
10823 editor.handle_input("{", window, cx);
10824 editor.handle_input("{", window, cx);
10825 editor.move_right(&MoveRight, window, cx);
10826 editor.move_right(&MoveRight, window, cx);
10827 editor.move_left(&MoveLeft, window, cx);
10828 editor.move_left(&MoveLeft, window, cx);
10829 editor.backspace(&Default::default(), window, cx);
10830 });
10831
10832 cx.assert_editor_state(
10833 &"
10834 {ˇ}
10835 {ˇ}]]
10836 {ˇ}
10837 "
10838 .unindent(),
10839 );
10840
10841 cx.update_editor(|editor, window, cx| {
10842 editor.backspace(&Default::default(), window, cx);
10843 });
10844
10845 cx.assert_editor_state(
10846 &"
10847 ˇ
10848 ˇ]]
10849 ˇ
10850 "
10851 .unindent(),
10852 );
10853}
10854
10855#[gpui::test]
10856async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10857 init_test(cx, |_| {});
10858
10859 let language = Arc::new(Language::new(
10860 LanguageConfig::default(),
10861 Some(tree_sitter_rust::LANGUAGE.into()),
10862 ));
10863
10864 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10865 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10866 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10867 editor
10868 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10869 .await;
10870
10871 editor.update_in(cx, |editor, window, cx| {
10872 editor.set_auto_replace_emoji_shortcode(true);
10873
10874 editor.handle_input("Hello ", window, cx);
10875 editor.handle_input(":wave", window, cx);
10876 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10877
10878 editor.handle_input(":", window, cx);
10879 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10880
10881 editor.handle_input(" :smile", window, cx);
10882 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10883
10884 editor.handle_input(":", window, cx);
10885 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10886
10887 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10888 editor.handle_input(":wave", window, cx);
10889 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10890
10891 editor.handle_input(":", window, cx);
10892 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10893
10894 editor.handle_input(":1", window, cx);
10895 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10896
10897 editor.handle_input(":", window, cx);
10898 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10899
10900 // Ensure shortcode does not get replaced when it is part of a word
10901 editor.handle_input(" Test:wave", window, cx);
10902 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
10903
10904 editor.handle_input(":", window, cx);
10905 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
10906
10907 editor.set_auto_replace_emoji_shortcode(false);
10908
10909 // Ensure shortcode does not get replaced when auto replace is off
10910 editor.handle_input(" :wave", window, cx);
10911 assert_eq!(
10912 editor.text(cx),
10913 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
10914 );
10915
10916 editor.handle_input(":", window, cx);
10917 assert_eq!(
10918 editor.text(cx),
10919 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
10920 );
10921 });
10922}
10923
10924#[gpui::test]
10925async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
10926 init_test(cx, |_| {});
10927
10928 let (text, insertion_ranges) = marked_text_ranges(
10929 indoc! {"
10930 ˇ
10931 "},
10932 false,
10933 );
10934
10935 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
10936 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10937
10938 _ = editor.update_in(cx, |editor, window, cx| {
10939 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
10940
10941 editor
10942 .insert_snippet(&insertion_ranges, snippet, window, cx)
10943 .unwrap();
10944
10945 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
10946 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
10947 assert_eq!(editor.text(cx), expected_text);
10948 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
10949 }
10950
10951 assert(
10952 editor,
10953 cx,
10954 indoc! {"
10955 type «» =•
10956 "},
10957 );
10958
10959 assert!(editor.context_menu_visible(), "There should be a matches");
10960 });
10961}
10962
10963#[gpui::test]
10964async fn test_snippets(cx: &mut TestAppContext) {
10965 init_test(cx, |_| {});
10966
10967 let mut cx = EditorTestContext::new(cx).await;
10968
10969 cx.set_state(indoc! {"
10970 a.ˇ b
10971 a.ˇ b
10972 a.ˇ b
10973 "});
10974
10975 cx.update_editor(|editor, window, cx| {
10976 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
10977 let insertion_ranges = editor
10978 .selections
10979 .all(cx)
10980 .iter()
10981 .map(|s| s.range())
10982 .collect::<Vec<_>>();
10983 editor
10984 .insert_snippet(&insertion_ranges, snippet, window, cx)
10985 .unwrap();
10986 });
10987
10988 cx.assert_editor_state(indoc! {"
10989 a.f(«oneˇ», two, «threeˇ») b
10990 a.f(«oneˇ», two, «threeˇ») b
10991 a.f(«oneˇ», two, «threeˇ») b
10992 "});
10993
10994 // Can't move earlier than the first tab stop
10995 cx.update_editor(|editor, window, cx| {
10996 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10997 });
10998 cx.assert_editor_state(indoc! {"
10999 a.f(«oneˇ», two, «threeˇ») b
11000 a.f(«oneˇ», two, «threeˇ») b
11001 a.f(«oneˇ», two, «threeˇ») b
11002 "});
11003
11004 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11005 cx.assert_editor_state(indoc! {"
11006 a.f(one, «twoˇ», three) b
11007 a.f(one, «twoˇ», three) b
11008 a.f(one, «twoˇ», three) b
11009 "});
11010
11011 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11012 cx.assert_editor_state(indoc! {"
11013 a.f(«oneˇ», two, «threeˇ») b
11014 a.f(«oneˇ», two, «threeˇ») b
11015 a.f(«oneˇ», two, «threeˇ») b
11016 "});
11017
11018 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11019 cx.assert_editor_state(indoc! {"
11020 a.f(one, «twoˇ», three) b
11021 a.f(one, «twoˇ», three) b
11022 a.f(one, «twoˇ», three) b
11023 "});
11024 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11025 cx.assert_editor_state(indoc! {"
11026 a.f(one, two, three)ˇ b
11027 a.f(one, two, three)ˇ b
11028 a.f(one, two, three)ˇ b
11029 "});
11030
11031 // As soon as the last tab stop is reached, snippet state is gone
11032 cx.update_editor(|editor, window, cx| {
11033 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11034 });
11035 cx.assert_editor_state(indoc! {"
11036 a.f(one, two, three)ˇ b
11037 a.f(one, two, three)ˇ b
11038 a.f(one, two, three)ˇ b
11039 "});
11040}
11041
11042#[gpui::test]
11043async fn test_snippet_indentation(cx: &mut TestAppContext) {
11044 init_test(cx, |_| {});
11045
11046 let mut cx = EditorTestContext::new(cx).await;
11047
11048 cx.update_editor(|editor, window, cx| {
11049 let snippet = Snippet::parse(indoc! {"
11050 /*
11051 * Multiline comment with leading indentation
11052 *
11053 * $1
11054 */
11055 $0"})
11056 .unwrap();
11057 let insertion_ranges = editor
11058 .selections
11059 .all(cx)
11060 .iter()
11061 .map(|s| s.range())
11062 .collect::<Vec<_>>();
11063 editor
11064 .insert_snippet(&insertion_ranges, snippet, window, cx)
11065 .unwrap();
11066 });
11067
11068 cx.assert_editor_state(indoc! {"
11069 /*
11070 * Multiline comment with leading indentation
11071 *
11072 * ˇ
11073 */
11074 "});
11075
11076 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11077 cx.assert_editor_state(indoc! {"
11078 /*
11079 * Multiline comment with leading indentation
11080 *
11081 *•
11082 */
11083 ˇ"});
11084}
11085
11086#[gpui::test]
11087async fn test_document_format_during_save(cx: &mut TestAppContext) {
11088 init_test(cx, |_| {});
11089
11090 let fs = FakeFs::new(cx.executor());
11091 fs.insert_file(path!("/file.rs"), Default::default()).await;
11092
11093 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11094
11095 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11096 language_registry.add(rust_lang());
11097 let mut fake_servers = language_registry.register_fake_lsp(
11098 "Rust",
11099 FakeLspAdapter {
11100 capabilities: lsp::ServerCapabilities {
11101 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11102 ..Default::default()
11103 },
11104 ..Default::default()
11105 },
11106 );
11107
11108 let buffer = project
11109 .update(cx, |project, cx| {
11110 project.open_local_buffer(path!("/file.rs"), cx)
11111 })
11112 .await
11113 .unwrap();
11114
11115 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11116 let (editor, cx) = cx.add_window_view(|window, cx| {
11117 build_editor_with_project(project.clone(), buffer, window, cx)
11118 });
11119 editor.update_in(cx, |editor, window, cx| {
11120 editor.set_text("one\ntwo\nthree\n", window, cx)
11121 });
11122 assert!(cx.read(|cx| editor.is_dirty(cx)));
11123
11124 cx.executor().start_waiting();
11125 let fake_server = fake_servers.next().await.unwrap();
11126
11127 {
11128 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11129 move |params, _| async move {
11130 assert_eq!(
11131 params.text_document.uri,
11132 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11133 );
11134 assert_eq!(params.options.tab_size, 4);
11135 Ok(Some(vec![lsp::TextEdit::new(
11136 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11137 ", ".to_string(),
11138 )]))
11139 },
11140 );
11141 let save = editor
11142 .update_in(cx, |editor, window, cx| {
11143 editor.save(
11144 SaveOptions {
11145 format: true,
11146 autosave: false,
11147 },
11148 project.clone(),
11149 window,
11150 cx,
11151 )
11152 })
11153 .unwrap();
11154 cx.executor().start_waiting();
11155 save.await;
11156
11157 assert_eq!(
11158 editor.update(cx, |editor, cx| editor.text(cx)),
11159 "one, two\nthree\n"
11160 );
11161 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11162 }
11163
11164 {
11165 editor.update_in(cx, |editor, window, cx| {
11166 editor.set_text("one\ntwo\nthree\n", window, cx)
11167 });
11168 assert!(cx.read(|cx| editor.is_dirty(cx)));
11169
11170 // Ensure we can still save even if formatting hangs.
11171 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11172 move |params, _| async move {
11173 assert_eq!(
11174 params.text_document.uri,
11175 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11176 );
11177 futures::future::pending::<()>().await;
11178 unreachable!()
11179 },
11180 );
11181 let save = editor
11182 .update_in(cx, |editor, window, cx| {
11183 editor.save(
11184 SaveOptions {
11185 format: true,
11186 autosave: false,
11187 },
11188 project.clone(),
11189 window,
11190 cx,
11191 )
11192 })
11193 .unwrap();
11194 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11195 cx.executor().start_waiting();
11196 save.await;
11197 assert_eq!(
11198 editor.update(cx, |editor, cx| editor.text(cx)),
11199 "one\ntwo\nthree\n"
11200 );
11201 }
11202
11203 // Set rust language override and assert overridden tabsize is sent to language server
11204 update_test_language_settings(cx, |settings| {
11205 settings.languages.0.insert(
11206 "Rust".into(),
11207 LanguageSettingsContent {
11208 tab_size: NonZeroU32::new(8),
11209 ..Default::default()
11210 },
11211 );
11212 });
11213
11214 {
11215 editor.update_in(cx, |editor, window, cx| {
11216 editor.set_text("somehting_new\n", window, cx)
11217 });
11218 assert!(cx.read(|cx| editor.is_dirty(cx)));
11219 let _formatting_request_signal = fake_server
11220 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11221 assert_eq!(
11222 params.text_document.uri,
11223 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11224 );
11225 assert_eq!(params.options.tab_size, 8);
11226 Ok(Some(vec![]))
11227 });
11228 let save = editor
11229 .update_in(cx, |editor, window, cx| {
11230 editor.save(
11231 SaveOptions {
11232 format: true,
11233 autosave: false,
11234 },
11235 project.clone(),
11236 window,
11237 cx,
11238 )
11239 })
11240 .unwrap();
11241 cx.executor().start_waiting();
11242 save.await;
11243 }
11244}
11245
11246#[gpui::test]
11247async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11248 init_test(cx, |settings| {
11249 settings.defaults.ensure_final_newline_on_save = Some(false);
11250 });
11251
11252 let fs = FakeFs::new(cx.executor());
11253 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11254
11255 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11256
11257 let buffer = project
11258 .update(cx, |project, cx| {
11259 project.open_local_buffer(path!("/file.txt"), cx)
11260 })
11261 .await
11262 .unwrap();
11263
11264 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11265 let (editor, cx) = cx.add_window_view(|window, cx| {
11266 build_editor_with_project(project.clone(), buffer, window, cx)
11267 });
11268 editor.update_in(cx, |editor, window, cx| {
11269 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11270 s.select_ranges([0..0])
11271 });
11272 });
11273 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11274
11275 editor.update_in(cx, |editor, window, cx| {
11276 editor.handle_input("\n", window, cx)
11277 });
11278 cx.run_until_parked();
11279 save(&editor, &project, cx).await;
11280 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11281
11282 editor.update_in(cx, |editor, window, cx| {
11283 editor.undo(&Default::default(), window, cx);
11284 });
11285 save(&editor, &project, cx).await;
11286 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11287
11288 editor.update_in(cx, |editor, window, cx| {
11289 editor.redo(&Default::default(), window, cx);
11290 });
11291 cx.run_until_parked();
11292 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11293
11294 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11295 let save = editor
11296 .update_in(cx, |editor, window, cx| {
11297 editor.save(
11298 SaveOptions {
11299 format: true,
11300 autosave: false,
11301 },
11302 project.clone(),
11303 window,
11304 cx,
11305 )
11306 })
11307 .unwrap();
11308 cx.executor().start_waiting();
11309 save.await;
11310 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11311 }
11312}
11313
11314#[gpui::test]
11315async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11316 init_test(cx, |_| {});
11317
11318 let cols = 4;
11319 let rows = 10;
11320 let sample_text_1 = sample_text(rows, cols, 'a');
11321 assert_eq!(
11322 sample_text_1,
11323 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11324 );
11325 let sample_text_2 = sample_text(rows, cols, 'l');
11326 assert_eq!(
11327 sample_text_2,
11328 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11329 );
11330 let sample_text_3 = sample_text(rows, cols, 'v');
11331 assert_eq!(
11332 sample_text_3,
11333 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11334 );
11335
11336 let fs = FakeFs::new(cx.executor());
11337 fs.insert_tree(
11338 path!("/a"),
11339 json!({
11340 "main.rs": sample_text_1,
11341 "other.rs": sample_text_2,
11342 "lib.rs": sample_text_3,
11343 }),
11344 )
11345 .await;
11346
11347 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11348 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11349 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11350
11351 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11352 language_registry.add(rust_lang());
11353 let mut fake_servers = language_registry.register_fake_lsp(
11354 "Rust",
11355 FakeLspAdapter {
11356 capabilities: lsp::ServerCapabilities {
11357 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11358 ..Default::default()
11359 },
11360 ..Default::default()
11361 },
11362 );
11363
11364 let worktree = project.update(cx, |project, cx| {
11365 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11366 assert_eq!(worktrees.len(), 1);
11367 worktrees.pop().unwrap()
11368 });
11369 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11370
11371 let buffer_1 = project
11372 .update(cx, |project, cx| {
11373 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11374 })
11375 .await
11376 .unwrap();
11377 let buffer_2 = project
11378 .update(cx, |project, cx| {
11379 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11380 })
11381 .await
11382 .unwrap();
11383 let buffer_3 = project
11384 .update(cx, |project, cx| {
11385 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11386 })
11387 .await
11388 .unwrap();
11389
11390 let multi_buffer = cx.new(|cx| {
11391 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11392 multi_buffer.push_excerpts(
11393 buffer_1.clone(),
11394 [
11395 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11396 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11397 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11398 ],
11399 cx,
11400 );
11401 multi_buffer.push_excerpts(
11402 buffer_2.clone(),
11403 [
11404 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11405 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11406 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11407 ],
11408 cx,
11409 );
11410 multi_buffer.push_excerpts(
11411 buffer_3.clone(),
11412 [
11413 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11414 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11415 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11416 ],
11417 cx,
11418 );
11419 multi_buffer
11420 });
11421 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11422 Editor::new(
11423 EditorMode::full(),
11424 multi_buffer,
11425 Some(project.clone()),
11426 window,
11427 cx,
11428 )
11429 });
11430
11431 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11432 editor.change_selections(
11433 SelectionEffects::scroll(Autoscroll::Next),
11434 window,
11435 cx,
11436 |s| s.select_ranges(Some(1..2)),
11437 );
11438 editor.insert("|one|two|three|", window, cx);
11439 });
11440 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11441 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11442 editor.change_selections(
11443 SelectionEffects::scroll(Autoscroll::Next),
11444 window,
11445 cx,
11446 |s| s.select_ranges(Some(60..70)),
11447 );
11448 editor.insert("|four|five|six|", window, cx);
11449 });
11450 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11451
11452 // First two buffers should be edited, but not the third one.
11453 assert_eq!(
11454 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11455 "a|one|two|three|aa\nbbbb\ncccc\n\nffff\ngggg\n\njjjj\nllll\nmmmm\nnnnn|four|five|six|\nr\n\nuuuu\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}",
11456 );
11457 buffer_1.update(cx, |buffer, _| {
11458 assert!(buffer.is_dirty());
11459 assert_eq!(
11460 buffer.text(),
11461 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11462 )
11463 });
11464 buffer_2.update(cx, |buffer, _| {
11465 assert!(buffer.is_dirty());
11466 assert_eq!(
11467 buffer.text(),
11468 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11469 )
11470 });
11471 buffer_3.update(cx, |buffer, _| {
11472 assert!(!buffer.is_dirty());
11473 assert_eq!(buffer.text(), sample_text_3,)
11474 });
11475 cx.executor().run_until_parked();
11476
11477 cx.executor().start_waiting();
11478 let save = multi_buffer_editor
11479 .update_in(cx, |editor, window, cx| {
11480 editor.save(
11481 SaveOptions {
11482 format: true,
11483 autosave: false,
11484 },
11485 project.clone(),
11486 window,
11487 cx,
11488 )
11489 })
11490 .unwrap();
11491
11492 let fake_server = fake_servers.next().await.unwrap();
11493 fake_server
11494 .server
11495 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11496 Ok(Some(vec![lsp::TextEdit::new(
11497 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11498 format!("[{} formatted]", params.text_document.uri),
11499 )]))
11500 })
11501 .detach();
11502 save.await;
11503
11504 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11505 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11506 assert_eq!(
11507 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11508 uri!(
11509 "a|o[file:///a/main.rs formatted]bbbb\ncccc\n\nffff\ngggg\n\njjjj\n\nlll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|\nr\n\nuuuu\n\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}"
11510 ),
11511 );
11512 buffer_1.update(cx, |buffer, _| {
11513 assert!(!buffer.is_dirty());
11514 assert_eq!(
11515 buffer.text(),
11516 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11517 )
11518 });
11519 buffer_2.update(cx, |buffer, _| {
11520 assert!(!buffer.is_dirty());
11521 assert_eq!(
11522 buffer.text(),
11523 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11524 )
11525 });
11526 buffer_3.update(cx, |buffer, _| {
11527 assert!(!buffer.is_dirty());
11528 assert_eq!(buffer.text(), sample_text_3,)
11529 });
11530}
11531
11532#[gpui::test]
11533async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11534 init_test(cx, |_| {});
11535
11536 let fs = FakeFs::new(cx.executor());
11537 fs.insert_tree(
11538 path!("/dir"),
11539 json!({
11540 "file1.rs": "fn main() { println!(\"hello\"); }",
11541 "file2.rs": "fn test() { println!(\"test\"); }",
11542 "file3.rs": "fn other() { println!(\"other\"); }\n",
11543 }),
11544 )
11545 .await;
11546
11547 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11548 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11549 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11550
11551 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11552 language_registry.add(rust_lang());
11553
11554 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11555 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11556
11557 // Open three buffers
11558 let buffer_1 = project
11559 .update(cx, |project, cx| {
11560 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11561 })
11562 .await
11563 .unwrap();
11564 let buffer_2 = project
11565 .update(cx, |project, cx| {
11566 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11567 })
11568 .await
11569 .unwrap();
11570 let buffer_3 = project
11571 .update(cx, |project, cx| {
11572 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11573 })
11574 .await
11575 .unwrap();
11576
11577 // Create a multi-buffer with all three buffers
11578 let multi_buffer = cx.new(|cx| {
11579 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11580 multi_buffer.push_excerpts(
11581 buffer_1.clone(),
11582 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11583 cx,
11584 );
11585 multi_buffer.push_excerpts(
11586 buffer_2.clone(),
11587 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11588 cx,
11589 );
11590 multi_buffer.push_excerpts(
11591 buffer_3.clone(),
11592 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11593 cx,
11594 );
11595 multi_buffer
11596 });
11597
11598 let editor = cx.new_window_entity(|window, cx| {
11599 Editor::new(
11600 EditorMode::full(),
11601 multi_buffer,
11602 Some(project.clone()),
11603 window,
11604 cx,
11605 )
11606 });
11607
11608 // Edit only the first buffer
11609 editor.update_in(cx, |editor, window, cx| {
11610 editor.change_selections(
11611 SelectionEffects::scroll(Autoscroll::Next),
11612 window,
11613 cx,
11614 |s| s.select_ranges(Some(10..10)),
11615 );
11616 editor.insert("// edited", window, cx);
11617 });
11618
11619 // Verify that only buffer 1 is dirty
11620 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11621 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11622 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11623
11624 // Get write counts after file creation (files were created with initial content)
11625 // We expect each file to have been written once during creation
11626 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11627 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11628 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11629
11630 // Perform autosave
11631 let save_task = editor.update_in(cx, |editor, window, cx| {
11632 editor.save(
11633 SaveOptions {
11634 format: true,
11635 autosave: true,
11636 },
11637 project.clone(),
11638 window,
11639 cx,
11640 )
11641 });
11642 save_task.await.unwrap();
11643
11644 // Only the dirty buffer should have been saved
11645 assert_eq!(
11646 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11647 1,
11648 "Buffer 1 was dirty, so it should have been written once during autosave"
11649 );
11650 assert_eq!(
11651 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11652 0,
11653 "Buffer 2 was clean, so it should not have been written during autosave"
11654 );
11655 assert_eq!(
11656 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11657 0,
11658 "Buffer 3 was clean, so it should not have been written during autosave"
11659 );
11660
11661 // Verify buffer states after autosave
11662 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11663 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11664 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11665
11666 // Now perform a manual save (format = true)
11667 let save_task = editor.update_in(cx, |editor, window, cx| {
11668 editor.save(
11669 SaveOptions {
11670 format: true,
11671 autosave: false,
11672 },
11673 project.clone(),
11674 window,
11675 cx,
11676 )
11677 });
11678 save_task.await.unwrap();
11679
11680 // During manual save, clean buffers don't get written to disk
11681 // They just get did_save called for language server notifications
11682 assert_eq!(
11683 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11684 1,
11685 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11686 );
11687 assert_eq!(
11688 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11689 0,
11690 "Buffer 2 should not have been written at all"
11691 );
11692 assert_eq!(
11693 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11694 0,
11695 "Buffer 3 should not have been written at all"
11696 );
11697}
11698
11699async fn setup_range_format_test(
11700 cx: &mut TestAppContext,
11701) -> (
11702 Entity<Project>,
11703 Entity<Editor>,
11704 &mut gpui::VisualTestContext,
11705 lsp::FakeLanguageServer,
11706) {
11707 init_test(cx, |_| {});
11708
11709 let fs = FakeFs::new(cx.executor());
11710 fs.insert_file(path!("/file.rs"), Default::default()).await;
11711
11712 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11713
11714 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11715 language_registry.add(rust_lang());
11716 let mut fake_servers = language_registry.register_fake_lsp(
11717 "Rust",
11718 FakeLspAdapter {
11719 capabilities: lsp::ServerCapabilities {
11720 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11721 ..lsp::ServerCapabilities::default()
11722 },
11723 ..FakeLspAdapter::default()
11724 },
11725 );
11726
11727 let buffer = project
11728 .update(cx, |project, cx| {
11729 project.open_local_buffer(path!("/file.rs"), cx)
11730 })
11731 .await
11732 .unwrap();
11733
11734 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11735 let (editor, cx) = cx.add_window_view(|window, cx| {
11736 build_editor_with_project(project.clone(), buffer, window, cx)
11737 });
11738
11739 cx.executor().start_waiting();
11740 let fake_server = fake_servers.next().await.unwrap();
11741
11742 (project, editor, cx, fake_server)
11743}
11744
11745#[gpui::test]
11746async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11747 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11748
11749 editor.update_in(cx, |editor, window, cx| {
11750 editor.set_text("one\ntwo\nthree\n", window, cx)
11751 });
11752 assert!(cx.read(|cx| editor.is_dirty(cx)));
11753
11754 let save = editor
11755 .update_in(cx, |editor, window, cx| {
11756 editor.save(
11757 SaveOptions {
11758 format: true,
11759 autosave: false,
11760 },
11761 project.clone(),
11762 window,
11763 cx,
11764 )
11765 })
11766 .unwrap();
11767 fake_server
11768 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11769 assert_eq!(
11770 params.text_document.uri,
11771 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11772 );
11773 assert_eq!(params.options.tab_size, 4);
11774 Ok(Some(vec![lsp::TextEdit::new(
11775 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11776 ", ".to_string(),
11777 )]))
11778 })
11779 .next()
11780 .await;
11781 cx.executor().start_waiting();
11782 save.await;
11783 assert_eq!(
11784 editor.update(cx, |editor, cx| editor.text(cx)),
11785 "one, two\nthree\n"
11786 );
11787 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11788}
11789
11790#[gpui::test]
11791async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11792 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11793
11794 editor.update_in(cx, |editor, window, cx| {
11795 editor.set_text("one\ntwo\nthree\n", window, cx)
11796 });
11797 assert!(cx.read(|cx| editor.is_dirty(cx)));
11798
11799 // Test that save still works when formatting hangs
11800 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11801 move |params, _| async move {
11802 assert_eq!(
11803 params.text_document.uri,
11804 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11805 );
11806 futures::future::pending::<()>().await;
11807 unreachable!()
11808 },
11809 );
11810 let save = editor
11811 .update_in(cx, |editor, window, cx| {
11812 editor.save(
11813 SaveOptions {
11814 format: true,
11815 autosave: false,
11816 },
11817 project.clone(),
11818 window,
11819 cx,
11820 )
11821 })
11822 .unwrap();
11823 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11824 cx.executor().start_waiting();
11825 save.await;
11826 assert_eq!(
11827 editor.update(cx, |editor, cx| editor.text(cx)),
11828 "one\ntwo\nthree\n"
11829 );
11830 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11831}
11832
11833#[gpui::test]
11834async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11835 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11836
11837 // Buffer starts clean, no formatting should be requested
11838 let save = editor
11839 .update_in(cx, |editor, window, cx| {
11840 editor.save(
11841 SaveOptions {
11842 format: false,
11843 autosave: false,
11844 },
11845 project.clone(),
11846 window,
11847 cx,
11848 )
11849 })
11850 .unwrap();
11851 let _pending_format_request = fake_server
11852 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11853 panic!("Should not be invoked");
11854 })
11855 .next();
11856 cx.executor().start_waiting();
11857 save.await;
11858 cx.run_until_parked();
11859}
11860
11861#[gpui::test]
11862async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11863 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11864
11865 // Set Rust language override and assert overridden tabsize is sent to language server
11866 update_test_language_settings(cx, |settings| {
11867 settings.languages.0.insert(
11868 "Rust".into(),
11869 LanguageSettingsContent {
11870 tab_size: NonZeroU32::new(8),
11871 ..Default::default()
11872 },
11873 );
11874 });
11875
11876 editor.update_in(cx, |editor, window, cx| {
11877 editor.set_text("something_new\n", window, cx)
11878 });
11879 assert!(cx.read(|cx| editor.is_dirty(cx)));
11880 let save = editor
11881 .update_in(cx, |editor, window, cx| {
11882 editor.save(
11883 SaveOptions {
11884 format: true,
11885 autosave: false,
11886 },
11887 project.clone(),
11888 window,
11889 cx,
11890 )
11891 })
11892 .unwrap();
11893 fake_server
11894 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11895 assert_eq!(
11896 params.text_document.uri,
11897 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11898 );
11899 assert_eq!(params.options.tab_size, 8);
11900 Ok(Some(Vec::new()))
11901 })
11902 .next()
11903 .await;
11904 save.await;
11905}
11906
11907#[gpui::test]
11908async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
11909 init_test(cx, |settings| {
11910 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
11911 settings::LanguageServerFormatterSpecifier::Current,
11912 )))
11913 });
11914
11915 let fs = FakeFs::new(cx.executor());
11916 fs.insert_file(path!("/file.rs"), Default::default()).await;
11917
11918 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11919
11920 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11921 language_registry.add(Arc::new(Language::new(
11922 LanguageConfig {
11923 name: "Rust".into(),
11924 matcher: LanguageMatcher {
11925 path_suffixes: vec!["rs".to_string()],
11926 ..Default::default()
11927 },
11928 ..LanguageConfig::default()
11929 },
11930 Some(tree_sitter_rust::LANGUAGE.into()),
11931 )));
11932 update_test_language_settings(cx, |settings| {
11933 // Enable Prettier formatting for the same buffer, and ensure
11934 // LSP is called instead of Prettier.
11935 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
11936 });
11937 let mut fake_servers = language_registry.register_fake_lsp(
11938 "Rust",
11939 FakeLspAdapter {
11940 capabilities: lsp::ServerCapabilities {
11941 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11942 ..Default::default()
11943 },
11944 ..Default::default()
11945 },
11946 );
11947
11948 let buffer = project
11949 .update(cx, |project, cx| {
11950 project.open_local_buffer(path!("/file.rs"), cx)
11951 })
11952 .await
11953 .unwrap();
11954
11955 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11956 let (editor, cx) = cx.add_window_view(|window, cx| {
11957 build_editor_with_project(project.clone(), buffer, window, cx)
11958 });
11959 editor.update_in(cx, |editor, window, cx| {
11960 editor.set_text("one\ntwo\nthree\n", window, cx)
11961 });
11962
11963 cx.executor().start_waiting();
11964 let fake_server = fake_servers.next().await.unwrap();
11965
11966 let format = editor
11967 .update_in(cx, |editor, window, cx| {
11968 editor.perform_format(
11969 project.clone(),
11970 FormatTrigger::Manual,
11971 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11972 window,
11973 cx,
11974 )
11975 })
11976 .unwrap();
11977 fake_server
11978 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11979 assert_eq!(
11980 params.text_document.uri,
11981 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11982 );
11983 assert_eq!(params.options.tab_size, 4);
11984 Ok(Some(vec![lsp::TextEdit::new(
11985 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11986 ", ".to_string(),
11987 )]))
11988 })
11989 .next()
11990 .await;
11991 cx.executor().start_waiting();
11992 format.await;
11993 assert_eq!(
11994 editor.update(cx, |editor, cx| editor.text(cx)),
11995 "one, two\nthree\n"
11996 );
11997
11998 editor.update_in(cx, |editor, window, cx| {
11999 editor.set_text("one\ntwo\nthree\n", window, cx)
12000 });
12001 // Ensure we don't lock if formatting hangs.
12002 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12003 move |params, _| async move {
12004 assert_eq!(
12005 params.text_document.uri,
12006 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12007 );
12008 futures::future::pending::<()>().await;
12009 unreachable!()
12010 },
12011 );
12012 let format = editor
12013 .update_in(cx, |editor, window, cx| {
12014 editor.perform_format(
12015 project,
12016 FormatTrigger::Manual,
12017 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12018 window,
12019 cx,
12020 )
12021 })
12022 .unwrap();
12023 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12024 cx.executor().start_waiting();
12025 format.await;
12026 assert_eq!(
12027 editor.update(cx, |editor, cx| editor.text(cx)),
12028 "one\ntwo\nthree\n"
12029 );
12030}
12031
12032#[gpui::test]
12033async fn test_multiple_formatters(cx: &mut TestAppContext) {
12034 init_test(cx, |settings| {
12035 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12036 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12037 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12038 Formatter::CodeAction("code-action-1".into()),
12039 Formatter::CodeAction("code-action-2".into()),
12040 ]))
12041 });
12042
12043 let fs = FakeFs::new(cx.executor());
12044 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12045 .await;
12046
12047 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12048 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12049 language_registry.add(rust_lang());
12050
12051 let mut fake_servers = language_registry.register_fake_lsp(
12052 "Rust",
12053 FakeLspAdapter {
12054 capabilities: lsp::ServerCapabilities {
12055 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12056 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12057 commands: vec!["the-command-for-code-action-1".into()],
12058 ..Default::default()
12059 }),
12060 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12061 ..Default::default()
12062 },
12063 ..Default::default()
12064 },
12065 );
12066
12067 let buffer = project
12068 .update(cx, |project, cx| {
12069 project.open_local_buffer(path!("/file.rs"), cx)
12070 })
12071 .await
12072 .unwrap();
12073
12074 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12075 let (editor, cx) = cx.add_window_view(|window, cx| {
12076 build_editor_with_project(project.clone(), buffer, window, cx)
12077 });
12078
12079 cx.executor().start_waiting();
12080
12081 let fake_server = fake_servers.next().await.unwrap();
12082 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12083 move |_params, _| async move {
12084 Ok(Some(vec![lsp::TextEdit::new(
12085 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12086 "applied-formatting\n".to_string(),
12087 )]))
12088 },
12089 );
12090 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12091 move |params, _| async move {
12092 let requested_code_actions = params.context.only.expect("Expected code action request");
12093 assert_eq!(requested_code_actions.len(), 1);
12094
12095 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12096 let code_action = match requested_code_actions[0].as_str() {
12097 "code-action-1" => lsp::CodeAction {
12098 kind: Some("code-action-1".into()),
12099 edit: Some(lsp::WorkspaceEdit::new(
12100 [(
12101 uri,
12102 vec![lsp::TextEdit::new(
12103 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12104 "applied-code-action-1-edit\n".to_string(),
12105 )],
12106 )]
12107 .into_iter()
12108 .collect(),
12109 )),
12110 command: Some(lsp::Command {
12111 command: "the-command-for-code-action-1".into(),
12112 ..Default::default()
12113 }),
12114 ..Default::default()
12115 },
12116 "code-action-2" => lsp::CodeAction {
12117 kind: Some("code-action-2".into()),
12118 edit: Some(lsp::WorkspaceEdit::new(
12119 [(
12120 uri,
12121 vec![lsp::TextEdit::new(
12122 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12123 "applied-code-action-2-edit\n".to_string(),
12124 )],
12125 )]
12126 .into_iter()
12127 .collect(),
12128 )),
12129 ..Default::default()
12130 },
12131 req => panic!("Unexpected code action request: {:?}", req),
12132 };
12133 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12134 code_action,
12135 )]))
12136 },
12137 );
12138
12139 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12140 move |params, _| async move { Ok(params) }
12141 });
12142
12143 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12144 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12145 let fake = fake_server.clone();
12146 let lock = command_lock.clone();
12147 move |params, _| {
12148 assert_eq!(params.command, "the-command-for-code-action-1");
12149 let fake = fake.clone();
12150 let lock = lock.clone();
12151 async move {
12152 lock.lock().await;
12153 fake.server
12154 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12155 label: None,
12156 edit: lsp::WorkspaceEdit {
12157 changes: Some(
12158 [(
12159 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12160 vec![lsp::TextEdit {
12161 range: lsp::Range::new(
12162 lsp::Position::new(0, 0),
12163 lsp::Position::new(0, 0),
12164 ),
12165 new_text: "applied-code-action-1-command\n".into(),
12166 }],
12167 )]
12168 .into_iter()
12169 .collect(),
12170 ),
12171 ..Default::default()
12172 },
12173 })
12174 .await
12175 .into_response()
12176 .unwrap();
12177 Ok(Some(json!(null)))
12178 }
12179 }
12180 });
12181
12182 cx.executor().start_waiting();
12183 editor
12184 .update_in(cx, |editor, window, cx| {
12185 editor.perform_format(
12186 project.clone(),
12187 FormatTrigger::Manual,
12188 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12189 window,
12190 cx,
12191 )
12192 })
12193 .unwrap()
12194 .await;
12195 editor.update(cx, |editor, cx| {
12196 assert_eq!(
12197 editor.text(cx),
12198 r#"
12199 applied-code-action-2-edit
12200 applied-code-action-1-command
12201 applied-code-action-1-edit
12202 applied-formatting
12203 one
12204 two
12205 three
12206 "#
12207 .unindent()
12208 );
12209 });
12210
12211 editor.update_in(cx, |editor, window, cx| {
12212 editor.undo(&Default::default(), window, cx);
12213 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12214 });
12215
12216 // Perform a manual edit while waiting for an LSP command
12217 // that's being run as part of a formatting code action.
12218 let lock_guard = command_lock.lock().await;
12219 let format = editor
12220 .update_in(cx, |editor, window, cx| {
12221 editor.perform_format(
12222 project.clone(),
12223 FormatTrigger::Manual,
12224 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12225 window,
12226 cx,
12227 )
12228 })
12229 .unwrap();
12230 cx.run_until_parked();
12231 editor.update(cx, |editor, cx| {
12232 assert_eq!(
12233 editor.text(cx),
12234 r#"
12235 applied-code-action-1-edit
12236 applied-formatting
12237 one
12238 two
12239 three
12240 "#
12241 .unindent()
12242 );
12243
12244 editor.buffer.update(cx, |buffer, cx| {
12245 let ix = buffer.len(cx);
12246 buffer.edit([(ix..ix, "edited\n")], None, cx);
12247 });
12248 });
12249
12250 // Allow the LSP command to proceed. Because the buffer was edited,
12251 // the second code action will not be run.
12252 drop(lock_guard);
12253 format.await;
12254 editor.update_in(cx, |editor, window, cx| {
12255 assert_eq!(
12256 editor.text(cx),
12257 r#"
12258 applied-code-action-1-command
12259 applied-code-action-1-edit
12260 applied-formatting
12261 one
12262 two
12263 three
12264 edited
12265 "#
12266 .unindent()
12267 );
12268
12269 // The manual edit is undone first, because it is the last thing the user did
12270 // (even though the command completed afterwards).
12271 editor.undo(&Default::default(), window, cx);
12272 assert_eq!(
12273 editor.text(cx),
12274 r#"
12275 applied-code-action-1-command
12276 applied-code-action-1-edit
12277 applied-formatting
12278 one
12279 two
12280 three
12281 "#
12282 .unindent()
12283 );
12284
12285 // All the formatting (including the command, which completed after the manual edit)
12286 // is undone together.
12287 editor.undo(&Default::default(), window, cx);
12288 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12289 });
12290}
12291
12292#[gpui::test]
12293async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12294 init_test(cx, |settings| {
12295 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12296 settings::LanguageServerFormatterSpecifier::Current,
12297 )]))
12298 });
12299
12300 let fs = FakeFs::new(cx.executor());
12301 fs.insert_file(path!("/file.ts"), Default::default()).await;
12302
12303 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12304
12305 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12306 language_registry.add(Arc::new(Language::new(
12307 LanguageConfig {
12308 name: "TypeScript".into(),
12309 matcher: LanguageMatcher {
12310 path_suffixes: vec!["ts".to_string()],
12311 ..Default::default()
12312 },
12313 ..LanguageConfig::default()
12314 },
12315 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12316 )));
12317 update_test_language_settings(cx, |settings| {
12318 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12319 });
12320 let mut fake_servers = language_registry.register_fake_lsp(
12321 "TypeScript",
12322 FakeLspAdapter {
12323 capabilities: lsp::ServerCapabilities {
12324 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12325 ..Default::default()
12326 },
12327 ..Default::default()
12328 },
12329 );
12330
12331 let buffer = project
12332 .update(cx, |project, cx| {
12333 project.open_local_buffer(path!("/file.ts"), cx)
12334 })
12335 .await
12336 .unwrap();
12337
12338 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12339 let (editor, cx) = cx.add_window_view(|window, cx| {
12340 build_editor_with_project(project.clone(), buffer, window, cx)
12341 });
12342 editor.update_in(cx, |editor, window, cx| {
12343 editor.set_text(
12344 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12345 window,
12346 cx,
12347 )
12348 });
12349
12350 cx.executor().start_waiting();
12351 let fake_server = fake_servers.next().await.unwrap();
12352
12353 let format = editor
12354 .update_in(cx, |editor, window, cx| {
12355 editor.perform_code_action_kind(
12356 project.clone(),
12357 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12358 window,
12359 cx,
12360 )
12361 })
12362 .unwrap();
12363 fake_server
12364 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12365 assert_eq!(
12366 params.text_document.uri,
12367 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12368 );
12369 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12370 lsp::CodeAction {
12371 title: "Organize Imports".to_string(),
12372 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12373 edit: Some(lsp::WorkspaceEdit {
12374 changes: Some(
12375 [(
12376 params.text_document.uri.clone(),
12377 vec![lsp::TextEdit::new(
12378 lsp::Range::new(
12379 lsp::Position::new(1, 0),
12380 lsp::Position::new(2, 0),
12381 ),
12382 "".to_string(),
12383 )],
12384 )]
12385 .into_iter()
12386 .collect(),
12387 ),
12388 ..Default::default()
12389 }),
12390 ..Default::default()
12391 },
12392 )]))
12393 })
12394 .next()
12395 .await;
12396 cx.executor().start_waiting();
12397 format.await;
12398 assert_eq!(
12399 editor.update(cx, |editor, cx| editor.text(cx)),
12400 "import { a } from 'module';\n\nconst x = a;\n"
12401 );
12402
12403 editor.update_in(cx, |editor, window, cx| {
12404 editor.set_text(
12405 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12406 window,
12407 cx,
12408 )
12409 });
12410 // Ensure we don't lock if code action hangs.
12411 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12412 move |params, _| async move {
12413 assert_eq!(
12414 params.text_document.uri,
12415 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12416 );
12417 futures::future::pending::<()>().await;
12418 unreachable!()
12419 },
12420 );
12421 let format = editor
12422 .update_in(cx, |editor, window, cx| {
12423 editor.perform_code_action_kind(
12424 project,
12425 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12426 window,
12427 cx,
12428 )
12429 })
12430 .unwrap();
12431 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12432 cx.executor().start_waiting();
12433 format.await;
12434 assert_eq!(
12435 editor.update(cx, |editor, cx| editor.text(cx)),
12436 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12437 );
12438}
12439
12440#[gpui::test]
12441async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12442 init_test(cx, |_| {});
12443
12444 let mut cx = EditorLspTestContext::new_rust(
12445 lsp::ServerCapabilities {
12446 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12447 ..Default::default()
12448 },
12449 cx,
12450 )
12451 .await;
12452
12453 cx.set_state(indoc! {"
12454 one.twoˇ
12455 "});
12456
12457 // The format request takes a long time. When it completes, it inserts
12458 // a newline and an indent before the `.`
12459 cx.lsp
12460 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12461 let executor = cx.background_executor().clone();
12462 async move {
12463 executor.timer(Duration::from_millis(100)).await;
12464 Ok(Some(vec![lsp::TextEdit {
12465 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12466 new_text: "\n ".into(),
12467 }]))
12468 }
12469 });
12470
12471 // Submit a format request.
12472 let format_1 = cx
12473 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12474 .unwrap();
12475 cx.executor().run_until_parked();
12476
12477 // Submit a second format request.
12478 let format_2 = cx
12479 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12480 .unwrap();
12481 cx.executor().run_until_parked();
12482
12483 // Wait for both format requests to complete
12484 cx.executor().advance_clock(Duration::from_millis(200));
12485 cx.executor().start_waiting();
12486 format_1.await.unwrap();
12487 cx.executor().start_waiting();
12488 format_2.await.unwrap();
12489
12490 // The formatting edits only happens once.
12491 cx.assert_editor_state(indoc! {"
12492 one
12493 .twoˇ
12494 "});
12495}
12496
12497#[gpui::test]
12498async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12499 init_test(cx, |settings| {
12500 settings.defaults.formatter = Some(FormatterList::default())
12501 });
12502
12503 let mut cx = EditorLspTestContext::new_rust(
12504 lsp::ServerCapabilities {
12505 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12506 ..Default::default()
12507 },
12508 cx,
12509 )
12510 .await;
12511
12512 cx.run_until_parked();
12513 // Set up a buffer white some trailing whitespace and no trailing newline.
12514 cx.set_state(
12515 &[
12516 "one ", //
12517 "twoˇ", //
12518 "three ", //
12519 "four", //
12520 ]
12521 .join("\n"),
12522 );
12523
12524 // Record which buffer changes have been sent to the language server
12525 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12526 cx.lsp
12527 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12528 let buffer_changes = buffer_changes.clone();
12529 move |params, _| {
12530 buffer_changes.lock().extend(
12531 params
12532 .content_changes
12533 .into_iter()
12534 .map(|e| (e.range.unwrap(), e.text)),
12535 );
12536 }
12537 });
12538
12539 // Handle formatting requests to the language server.
12540 cx.lsp
12541 .set_request_handler::<lsp::request::Formatting, _, _>({
12542 let buffer_changes = buffer_changes.clone();
12543 move |_, _| {
12544 let buffer_changes = buffer_changes.clone();
12545 // Insert blank lines between each line of the buffer.
12546 async move {
12547 // When formatting is requested, trailing whitespace has already been stripped,
12548 // and the trailing newline has already been added.
12549 assert_eq!(
12550 &buffer_changes.lock()[1..],
12551 &[
12552 (
12553 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12554 "".into()
12555 ),
12556 (
12557 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12558 "".into()
12559 ),
12560 (
12561 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12562 "\n".into()
12563 ),
12564 ]
12565 );
12566
12567 Ok(Some(vec![
12568 lsp::TextEdit {
12569 range: lsp::Range::new(
12570 lsp::Position::new(1, 0),
12571 lsp::Position::new(1, 0),
12572 ),
12573 new_text: "\n".into(),
12574 },
12575 lsp::TextEdit {
12576 range: lsp::Range::new(
12577 lsp::Position::new(2, 0),
12578 lsp::Position::new(2, 0),
12579 ),
12580 new_text: "\n".into(),
12581 },
12582 ]))
12583 }
12584 }
12585 });
12586
12587 // Submit a format request.
12588 let format = cx
12589 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12590 .unwrap();
12591
12592 cx.run_until_parked();
12593 // After formatting the buffer, the trailing whitespace is stripped,
12594 // a newline is appended, and the edits provided by the language server
12595 // have been applied.
12596 format.await.unwrap();
12597
12598 cx.assert_editor_state(
12599 &[
12600 "one", //
12601 "", //
12602 "twoˇ", //
12603 "", //
12604 "three", //
12605 "four", //
12606 "", //
12607 ]
12608 .join("\n"),
12609 );
12610
12611 // Undoing the formatting undoes the trailing whitespace removal, the
12612 // trailing newline, and the LSP edits.
12613 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12614 cx.assert_editor_state(
12615 &[
12616 "one ", //
12617 "twoˇ", //
12618 "three ", //
12619 "four", //
12620 ]
12621 .join("\n"),
12622 );
12623}
12624
12625#[gpui::test]
12626async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12627 cx: &mut TestAppContext,
12628) {
12629 init_test(cx, |_| {});
12630
12631 cx.update(|cx| {
12632 cx.update_global::<SettingsStore, _>(|settings, cx| {
12633 settings.update_user_settings(cx, |settings| {
12634 settings.editor.auto_signature_help = Some(true);
12635 });
12636 });
12637 });
12638
12639 let mut cx = EditorLspTestContext::new_rust(
12640 lsp::ServerCapabilities {
12641 signature_help_provider: Some(lsp::SignatureHelpOptions {
12642 ..Default::default()
12643 }),
12644 ..Default::default()
12645 },
12646 cx,
12647 )
12648 .await;
12649
12650 let language = Language::new(
12651 LanguageConfig {
12652 name: "Rust".into(),
12653 brackets: BracketPairConfig {
12654 pairs: vec![
12655 BracketPair {
12656 start: "{".to_string(),
12657 end: "}".to_string(),
12658 close: true,
12659 surround: true,
12660 newline: true,
12661 },
12662 BracketPair {
12663 start: "(".to_string(),
12664 end: ")".to_string(),
12665 close: true,
12666 surround: true,
12667 newline: true,
12668 },
12669 BracketPair {
12670 start: "/*".to_string(),
12671 end: " */".to_string(),
12672 close: true,
12673 surround: true,
12674 newline: true,
12675 },
12676 BracketPair {
12677 start: "[".to_string(),
12678 end: "]".to_string(),
12679 close: false,
12680 surround: false,
12681 newline: true,
12682 },
12683 BracketPair {
12684 start: "\"".to_string(),
12685 end: "\"".to_string(),
12686 close: true,
12687 surround: true,
12688 newline: false,
12689 },
12690 BracketPair {
12691 start: "<".to_string(),
12692 end: ">".to_string(),
12693 close: false,
12694 surround: true,
12695 newline: true,
12696 },
12697 ],
12698 ..Default::default()
12699 },
12700 autoclose_before: "})]".to_string(),
12701 ..Default::default()
12702 },
12703 Some(tree_sitter_rust::LANGUAGE.into()),
12704 );
12705 let language = Arc::new(language);
12706
12707 cx.language_registry().add(language.clone());
12708 cx.update_buffer(|buffer, cx| {
12709 buffer.set_language(Some(language), cx);
12710 });
12711
12712 cx.set_state(
12713 &r#"
12714 fn main() {
12715 sampleˇ
12716 }
12717 "#
12718 .unindent(),
12719 );
12720
12721 cx.update_editor(|editor, window, cx| {
12722 editor.handle_input("(", window, cx);
12723 });
12724 cx.assert_editor_state(
12725 &"
12726 fn main() {
12727 sample(ˇ)
12728 }
12729 "
12730 .unindent(),
12731 );
12732
12733 let mocked_response = lsp::SignatureHelp {
12734 signatures: vec![lsp::SignatureInformation {
12735 label: "fn sample(param1: u8, param2: u8)".to_string(),
12736 documentation: None,
12737 parameters: Some(vec![
12738 lsp::ParameterInformation {
12739 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12740 documentation: None,
12741 },
12742 lsp::ParameterInformation {
12743 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12744 documentation: None,
12745 },
12746 ]),
12747 active_parameter: None,
12748 }],
12749 active_signature: Some(0),
12750 active_parameter: Some(0),
12751 };
12752 handle_signature_help_request(&mut cx, mocked_response).await;
12753
12754 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12755 .await;
12756
12757 cx.editor(|editor, _, _| {
12758 let signature_help_state = editor.signature_help_state.popover().cloned();
12759 let signature = signature_help_state.unwrap();
12760 assert_eq!(
12761 signature.signatures[signature.current_signature].label,
12762 "fn sample(param1: u8, param2: u8)"
12763 );
12764 });
12765}
12766
12767#[gpui::test]
12768async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12769 init_test(cx, |_| {});
12770
12771 cx.update(|cx| {
12772 cx.update_global::<SettingsStore, _>(|settings, cx| {
12773 settings.update_user_settings(cx, |settings| {
12774 settings.editor.auto_signature_help = Some(false);
12775 settings.editor.show_signature_help_after_edits = Some(false);
12776 });
12777 });
12778 });
12779
12780 let mut cx = EditorLspTestContext::new_rust(
12781 lsp::ServerCapabilities {
12782 signature_help_provider: Some(lsp::SignatureHelpOptions {
12783 ..Default::default()
12784 }),
12785 ..Default::default()
12786 },
12787 cx,
12788 )
12789 .await;
12790
12791 let language = Language::new(
12792 LanguageConfig {
12793 name: "Rust".into(),
12794 brackets: BracketPairConfig {
12795 pairs: vec![
12796 BracketPair {
12797 start: "{".to_string(),
12798 end: "}".to_string(),
12799 close: true,
12800 surround: true,
12801 newline: true,
12802 },
12803 BracketPair {
12804 start: "(".to_string(),
12805 end: ")".to_string(),
12806 close: true,
12807 surround: true,
12808 newline: true,
12809 },
12810 BracketPair {
12811 start: "/*".to_string(),
12812 end: " */".to_string(),
12813 close: true,
12814 surround: true,
12815 newline: true,
12816 },
12817 BracketPair {
12818 start: "[".to_string(),
12819 end: "]".to_string(),
12820 close: false,
12821 surround: false,
12822 newline: true,
12823 },
12824 BracketPair {
12825 start: "\"".to_string(),
12826 end: "\"".to_string(),
12827 close: true,
12828 surround: true,
12829 newline: false,
12830 },
12831 BracketPair {
12832 start: "<".to_string(),
12833 end: ">".to_string(),
12834 close: false,
12835 surround: true,
12836 newline: true,
12837 },
12838 ],
12839 ..Default::default()
12840 },
12841 autoclose_before: "})]".to_string(),
12842 ..Default::default()
12843 },
12844 Some(tree_sitter_rust::LANGUAGE.into()),
12845 );
12846 let language = Arc::new(language);
12847
12848 cx.language_registry().add(language.clone());
12849 cx.update_buffer(|buffer, cx| {
12850 buffer.set_language(Some(language), cx);
12851 });
12852
12853 // Ensure that signature_help is not called when no signature help is enabled.
12854 cx.set_state(
12855 &r#"
12856 fn main() {
12857 sampleˇ
12858 }
12859 "#
12860 .unindent(),
12861 );
12862 cx.update_editor(|editor, window, cx| {
12863 editor.handle_input("(", window, cx);
12864 });
12865 cx.assert_editor_state(
12866 &"
12867 fn main() {
12868 sample(ˇ)
12869 }
12870 "
12871 .unindent(),
12872 );
12873 cx.editor(|editor, _, _| {
12874 assert!(editor.signature_help_state.task().is_none());
12875 });
12876
12877 let mocked_response = lsp::SignatureHelp {
12878 signatures: vec![lsp::SignatureInformation {
12879 label: "fn sample(param1: u8, param2: u8)".to_string(),
12880 documentation: None,
12881 parameters: Some(vec![
12882 lsp::ParameterInformation {
12883 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12884 documentation: None,
12885 },
12886 lsp::ParameterInformation {
12887 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12888 documentation: None,
12889 },
12890 ]),
12891 active_parameter: None,
12892 }],
12893 active_signature: Some(0),
12894 active_parameter: Some(0),
12895 };
12896
12897 // Ensure that signature_help is called when enabled afte edits
12898 cx.update(|_, cx| {
12899 cx.update_global::<SettingsStore, _>(|settings, cx| {
12900 settings.update_user_settings(cx, |settings| {
12901 settings.editor.auto_signature_help = Some(false);
12902 settings.editor.show_signature_help_after_edits = Some(true);
12903 });
12904 });
12905 });
12906 cx.set_state(
12907 &r#"
12908 fn main() {
12909 sampleˇ
12910 }
12911 "#
12912 .unindent(),
12913 );
12914 cx.update_editor(|editor, window, cx| {
12915 editor.handle_input("(", window, cx);
12916 });
12917 cx.assert_editor_state(
12918 &"
12919 fn main() {
12920 sample(ˇ)
12921 }
12922 "
12923 .unindent(),
12924 );
12925 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12926 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12927 .await;
12928 cx.update_editor(|editor, _, _| {
12929 let signature_help_state = editor.signature_help_state.popover().cloned();
12930 assert!(signature_help_state.is_some());
12931 let signature = signature_help_state.unwrap();
12932 assert_eq!(
12933 signature.signatures[signature.current_signature].label,
12934 "fn sample(param1: u8, param2: u8)"
12935 );
12936 editor.signature_help_state = SignatureHelpState::default();
12937 });
12938
12939 // Ensure that signature_help is called when auto signature help override is enabled
12940 cx.update(|_, cx| {
12941 cx.update_global::<SettingsStore, _>(|settings, cx| {
12942 settings.update_user_settings(cx, |settings| {
12943 settings.editor.auto_signature_help = Some(true);
12944 settings.editor.show_signature_help_after_edits = Some(false);
12945 });
12946 });
12947 });
12948 cx.set_state(
12949 &r#"
12950 fn main() {
12951 sampleˇ
12952 }
12953 "#
12954 .unindent(),
12955 );
12956 cx.update_editor(|editor, window, cx| {
12957 editor.handle_input("(", window, cx);
12958 });
12959 cx.assert_editor_state(
12960 &"
12961 fn main() {
12962 sample(ˇ)
12963 }
12964 "
12965 .unindent(),
12966 );
12967 handle_signature_help_request(&mut cx, mocked_response).await;
12968 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12969 .await;
12970 cx.editor(|editor, _, _| {
12971 let signature_help_state = editor.signature_help_state.popover().cloned();
12972 assert!(signature_help_state.is_some());
12973 let signature = signature_help_state.unwrap();
12974 assert_eq!(
12975 signature.signatures[signature.current_signature].label,
12976 "fn sample(param1: u8, param2: u8)"
12977 );
12978 });
12979}
12980
12981#[gpui::test]
12982async fn test_signature_help(cx: &mut TestAppContext) {
12983 init_test(cx, |_| {});
12984 cx.update(|cx| {
12985 cx.update_global::<SettingsStore, _>(|settings, cx| {
12986 settings.update_user_settings(cx, |settings| {
12987 settings.editor.auto_signature_help = Some(true);
12988 });
12989 });
12990 });
12991
12992 let mut cx = EditorLspTestContext::new_rust(
12993 lsp::ServerCapabilities {
12994 signature_help_provider: Some(lsp::SignatureHelpOptions {
12995 ..Default::default()
12996 }),
12997 ..Default::default()
12998 },
12999 cx,
13000 )
13001 .await;
13002
13003 // A test that directly calls `show_signature_help`
13004 cx.update_editor(|editor, window, cx| {
13005 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13006 });
13007
13008 let mocked_response = lsp::SignatureHelp {
13009 signatures: vec![lsp::SignatureInformation {
13010 label: "fn sample(param1: u8, param2: u8)".to_string(),
13011 documentation: None,
13012 parameters: Some(vec![
13013 lsp::ParameterInformation {
13014 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13015 documentation: None,
13016 },
13017 lsp::ParameterInformation {
13018 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13019 documentation: None,
13020 },
13021 ]),
13022 active_parameter: None,
13023 }],
13024 active_signature: Some(0),
13025 active_parameter: Some(0),
13026 };
13027 handle_signature_help_request(&mut cx, mocked_response).await;
13028
13029 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13030 .await;
13031
13032 cx.editor(|editor, _, _| {
13033 let signature_help_state = editor.signature_help_state.popover().cloned();
13034 assert!(signature_help_state.is_some());
13035 let signature = signature_help_state.unwrap();
13036 assert_eq!(
13037 signature.signatures[signature.current_signature].label,
13038 "fn sample(param1: u8, param2: u8)"
13039 );
13040 });
13041
13042 // When exiting outside from inside the brackets, `signature_help` is closed.
13043 cx.set_state(indoc! {"
13044 fn main() {
13045 sample(ˇ);
13046 }
13047
13048 fn sample(param1: u8, param2: u8) {}
13049 "});
13050
13051 cx.update_editor(|editor, window, cx| {
13052 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13053 s.select_ranges([0..0])
13054 });
13055 });
13056
13057 let mocked_response = lsp::SignatureHelp {
13058 signatures: Vec::new(),
13059 active_signature: None,
13060 active_parameter: None,
13061 };
13062 handle_signature_help_request(&mut cx, mocked_response).await;
13063
13064 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13065 .await;
13066
13067 cx.editor(|editor, _, _| {
13068 assert!(!editor.signature_help_state.is_shown());
13069 });
13070
13071 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13072 cx.set_state(indoc! {"
13073 fn main() {
13074 sample(ˇ);
13075 }
13076
13077 fn sample(param1: u8, param2: u8) {}
13078 "});
13079
13080 let mocked_response = lsp::SignatureHelp {
13081 signatures: vec![lsp::SignatureInformation {
13082 label: "fn sample(param1: u8, param2: u8)".to_string(),
13083 documentation: None,
13084 parameters: Some(vec![
13085 lsp::ParameterInformation {
13086 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13087 documentation: None,
13088 },
13089 lsp::ParameterInformation {
13090 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13091 documentation: None,
13092 },
13093 ]),
13094 active_parameter: None,
13095 }],
13096 active_signature: Some(0),
13097 active_parameter: Some(0),
13098 };
13099 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13100 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13101 .await;
13102 cx.editor(|editor, _, _| {
13103 assert!(editor.signature_help_state.is_shown());
13104 });
13105
13106 // Restore the popover with more parameter input
13107 cx.set_state(indoc! {"
13108 fn main() {
13109 sample(param1, param2ˇ);
13110 }
13111
13112 fn sample(param1: u8, param2: u8) {}
13113 "});
13114
13115 let mocked_response = lsp::SignatureHelp {
13116 signatures: vec![lsp::SignatureInformation {
13117 label: "fn sample(param1: u8, param2: u8)".to_string(),
13118 documentation: None,
13119 parameters: Some(vec![
13120 lsp::ParameterInformation {
13121 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13122 documentation: None,
13123 },
13124 lsp::ParameterInformation {
13125 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13126 documentation: None,
13127 },
13128 ]),
13129 active_parameter: None,
13130 }],
13131 active_signature: Some(0),
13132 active_parameter: Some(1),
13133 };
13134 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13135 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13136 .await;
13137
13138 // When selecting a range, the popover is gone.
13139 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13140 cx.update_editor(|editor, window, cx| {
13141 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13142 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13143 })
13144 });
13145 cx.assert_editor_state(indoc! {"
13146 fn main() {
13147 sample(param1, «ˇparam2»);
13148 }
13149
13150 fn sample(param1: u8, param2: u8) {}
13151 "});
13152 cx.editor(|editor, _, _| {
13153 assert!(!editor.signature_help_state.is_shown());
13154 });
13155
13156 // When unselecting again, the popover is back if within the brackets.
13157 cx.update_editor(|editor, window, cx| {
13158 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13159 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13160 })
13161 });
13162 cx.assert_editor_state(indoc! {"
13163 fn main() {
13164 sample(param1, ˇparam2);
13165 }
13166
13167 fn sample(param1: u8, param2: u8) {}
13168 "});
13169 handle_signature_help_request(&mut cx, mocked_response).await;
13170 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13171 .await;
13172 cx.editor(|editor, _, _| {
13173 assert!(editor.signature_help_state.is_shown());
13174 });
13175
13176 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13177 cx.update_editor(|editor, window, cx| {
13178 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13179 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13180 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13181 })
13182 });
13183 cx.assert_editor_state(indoc! {"
13184 fn main() {
13185 sample(param1, ˇparam2);
13186 }
13187
13188 fn sample(param1: u8, param2: u8) {}
13189 "});
13190
13191 let mocked_response = lsp::SignatureHelp {
13192 signatures: vec![lsp::SignatureInformation {
13193 label: "fn sample(param1: u8, param2: u8)".to_string(),
13194 documentation: None,
13195 parameters: Some(vec![
13196 lsp::ParameterInformation {
13197 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13198 documentation: None,
13199 },
13200 lsp::ParameterInformation {
13201 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13202 documentation: None,
13203 },
13204 ]),
13205 active_parameter: None,
13206 }],
13207 active_signature: Some(0),
13208 active_parameter: Some(1),
13209 };
13210 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13211 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13212 .await;
13213 cx.update_editor(|editor, _, cx| {
13214 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13215 });
13216 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13217 .await;
13218 cx.update_editor(|editor, window, cx| {
13219 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13220 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13221 })
13222 });
13223 cx.assert_editor_state(indoc! {"
13224 fn main() {
13225 sample(param1, «ˇparam2»);
13226 }
13227
13228 fn sample(param1: u8, param2: u8) {}
13229 "});
13230 cx.update_editor(|editor, window, cx| {
13231 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13232 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13233 })
13234 });
13235 cx.assert_editor_state(indoc! {"
13236 fn main() {
13237 sample(param1, ˇparam2);
13238 }
13239
13240 fn sample(param1: u8, param2: u8) {}
13241 "});
13242 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13243 .await;
13244}
13245
13246#[gpui::test]
13247async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13248 init_test(cx, |_| {});
13249
13250 let mut cx = EditorLspTestContext::new_rust(
13251 lsp::ServerCapabilities {
13252 signature_help_provider: Some(lsp::SignatureHelpOptions {
13253 ..Default::default()
13254 }),
13255 ..Default::default()
13256 },
13257 cx,
13258 )
13259 .await;
13260
13261 cx.set_state(indoc! {"
13262 fn main() {
13263 overloadedˇ
13264 }
13265 "});
13266
13267 cx.update_editor(|editor, window, cx| {
13268 editor.handle_input("(", window, cx);
13269 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13270 });
13271
13272 // Mock response with 3 signatures
13273 let mocked_response = lsp::SignatureHelp {
13274 signatures: vec![
13275 lsp::SignatureInformation {
13276 label: "fn overloaded(x: i32)".to_string(),
13277 documentation: None,
13278 parameters: Some(vec![lsp::ParameterInformation {
13279 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13280 documentation: None,
13281 }]),
13282 active_parameter: None,
13283 },
13284 lsp::SignatureInformation {
13285 label: "fn overloaded(x: i32, y: i32)".to_string(),
13286 documentation: None,
13287 parameters: Some(vec![
13288 lsp::ParameterInformation {
13289 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13290 documentation: None,
13291 },
13292 lsp::ParameterInformation {
13293 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13294 documentation: None,
13295 },
13296 ]),
13297 active_parameter: None,
13298 },
13299 lsp::SignatureInformation {
13300 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13301 documentation: None,
13302 parameters: Some(vec![
13303 lsp::ParameterInformation {
13304 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13305 documentation: None,
13306 },
13307 lsp::ParameterInformation {
13308 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13309 documentation: None,
13310 },
13311 lsp::ParameterInformation {
13312 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13313 documentation: None,
13314 },
13315 ]),
13316 active_parameter: None,
13317 },
13318 ],
13319 active_signature: Some(1),
13320 active_parameter: Some(0),
13321 };
13322 handle_signature_help_request(&mut cx, mocked_response).await;
13323
13324 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13325 .await;
13326
13327 // Verify we have multiple signatures and the right one is selected
13328 cx.editor(|editor, _, _| {
13329 let popover = editor.signature_help_state.popover().cloned().unwrap();
13330 assert_eq!(popover.signatures.len(), 3);
13331 // active_signature was 1, so that should be the current
13332 assert_eq!(popover.current_signature, 1);
13333 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13334 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13335 assert_eq!(
13336 popover.signatures[2].label,
13337 "fn overloaded(x: i32, y: i32, z: i32)"
13338 );
13339 });
13340
13341 // Test navigation functionality
13342 cx.update_editor(|editor, window, cx| {
13343 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13344 });
13345
13346 cx.editor(|editor, _, _| {
13347 let popover = editor.signature_help_state.popover().cloned().unwrap();
13348 assert_eq!(popover.current_signature, 2);
13349 });
13350
13351 // Test wrap around
13352 cx.update_editor(|editor, window, cx| {
13353 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13354 });
13355
13356 cx.editor(|editor, _, _| {
13357 let popover = editor.signature_help_state.popover().cloned().unwrap();
13358 assert_eq!(popover.current_signature, 0);
13359 });
13360
13361 // Test previous navigation
13362 cx.update_editor(|editor, window, cx| {
13363 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13364 });
13365
13366 cx.editor(|editor, _, _| {
13367 let popover = editor.signature_help_state.popover().cloned().unwrap();
13368 assert_eq!(popover.current_signature, 2);
13369 });
13370}
13371
13372#[gpui::test]
13373async fn test_completion_mode(cx: &mut TestAppContext) {
13374 init_test(cx, |_| {});
13375 let mut cx = EditorLspTestContext::new_rust(
13376 lsp::ServerCapabilities {
13377 completion_provider: Some(lsp::CompletionOptions {
13378 resolve_provider: Some(true),
13379 ..Default::default()
13380 }),
13381 ..Default::default()
13382 },
13383 cx,
13384 )
13385 .await;
13386
13387 struct Run {
13388 run_description: &'static str,
13389 initial_state: String,
13390 buffer_marked_text: String,
13391 completion_label: &'static str,
13392 completion_text: &'static str,
13393 expected_with_insert_mode: String,
13394 expected_with_replace_mode: String,
13395 expected_with_replace_subsequence_mode: String,
13396 expected_with_replace_suffix_mode: String,
13397 }
13398
13399 let runs = [
13400 Run {
13401 run_description: "Start of word matches completion text",
13402 initial_state: "before ediˇ after".into(),
13403 buffer_marked_text: "before <edi|> after".into(),
13404 completion_label: "editor",
13405 completion_text: "editor",
13406 expected_with_insert_mode: "before editorˇ after".into(),
13407 expected_with_replace_mode: "before editorˇ after".into(),
13408 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13409 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13410 },
13411 Run {
13412 run_description: "Accept same text at the middle of the word",
13413 initial_state: "before ediˇtor after".into(),
13414 buffer_marked_text: "before <edi|tor> after".into(),
13415 completion_label: "editor",
13416 completion_text: "editor",
13417 expected_with_insert_mode: "before editorˇtor after".into(),
13418 expected_with_replace_mode: "before editorˇ after".into(),
13419 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13420 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13421 },
13422 Run {
13423 run_description: "End of word matches completion text -- cursor at end",
13424 initial_state: "before torˇ after".into(),
13425 buffer_marked_text: "before <tor|> after".into(),
13426 completion_label: "editor",
13427 completion_text: "editor",
13428 expected_with_insert_mode: "before editorˇ after".into(),
13429 expected_with_replace_mode: "before editorˇ after".into(),
13430 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13431 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13432 },
13433 Run {
13434 run_description: "End of word matches completion text -- cursor at start",
13435 initial_state: "before ˇtor after".into(),
13436 buffer_marked_text: "before <|tor> after".into(),
13437 completion_label: "editor",
13438 completion_text: "editor",
13439 expected_with_insert_mode: "before editorˇtor after".into(),
13440 expected_with_replace_mode: "before editorˇ after".into(),
13441 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13442 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13443 },
13444 Run {
13445 run_description: "Prepend text containing whitespace",
13446 initial_state: "pˇfield: bool".into(),
13447 buffer_marked_text: "<p|field>: bool".into(),
13448 completion_label: "pub ",
13449 completion_text: "pub ",
13450 expected_with_insert_mode: "pub ˇfield: bool".into(),
13451 expected_with_replace_mode: "pub ˇ: bool".into(),
13452 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13453 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13454 },
13455 Run {
13456 run_description: "Add element to start of list",
13457 initial_state: "[element_ˇelement_2]".into(),
13458 buffer_marked_text: "[<element_|element_2>]".into(),
13459 completion_label: "element_1",
13460 completion_text: "element_1",
13461 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13462 expected_with_replace_mode: "[element_1ˇ]".into(),
13463 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13464 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13465 },
13466 Run {
13467 run_description: "Add element to start of list -- first and second elements are equal",
13468 initial_state: "[elˇelement]".into(),
13469 buffer_marked_text: "[<el|element>]".into(),
13470 completion_label: "element",
13471 completion_text: "element",
13472 expected_with_insert_mode: "[elementˇelement]".into(),
13473 expected_with_replace_mode: "[elementˇ]".into(),
13474 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13475 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13476 },
13477 Run {
13478 run_description: "Ends with matching suffix",
13479 initial_state: "SubˇError".into(),
13480 buffer_marked_text: "<Sub|Error>".into(),
13481 completion_label: "SubscriptionError",
13482 completion_text: "SubscriptionError",
13483 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13484 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13485 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13486 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13487 },
13488 Run {
13489 run_description: "Suffix is a subsequence -- contiguous",
13490 initial_state: "SubˇErr".into(),
13491 buffer_marked_text: "<Sub|Err>".into(),
13492 completion_label: "SubscriptionError",
13493 completion_text: "SubscriptionError",
13494 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13495 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13496 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13497 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13498 },
13499 Run {
13500 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13501 initial_state: "Suˇscrirr".into(),
13502 buffer_marked_text: "<Su|scrirr>".into(),
13503 completion_label: "SubscriptionError",
13504 completion_text: "SubscriptionError",
13505 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13506 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13507 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13508 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13509 },
13510 Run {
13511 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13512 initial_state: "foo(indˇix)".into(),
13513 buffer_marked_text: "foo(<ind|ix>)".into(),
13514 completion_label: "node_index",
13515 completion_text: "node_index",
13516 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13517 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13518 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13519 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13520 },
13521 Run {
13522 run_description: "Replace range ends before cursor - should extend to cursor",
13523 initial_state: "before editˇo after".into(),
13524 buffer_marked_text: "before <{ed}>it|o after".into(),
13525 completion_label: "editor",
13526 completion_text: "editor",
13527 expected_with_insert_mode: "before editorˇo after".into(),
13528 expected_with_replace_mode: "before editorˇo after".into(),
13529 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13530 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13531 },
13532 Run {
13533 run_description: "Uses label for suffix matching",
13534 initial_state: "before ediˇtor after".into(),
13535 buffer_marked_text: "before <edi|tor> after".into(),
13536 completion_label: "editor",
13537 completion_text: "editor()",
13538 expected_with_insert_mode: "before editor()ˇtor after".into(),
13539 expected_with_replace_mode: "before editor()ˇ after".into(),
13540 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13541 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13542 },
13543 Run {
13544 run_description: "Case insensitive subsequence and suffix matching",
13545 initial_state: "before EDiˇtoR after".into(),
13546 buffer_marked_text: "before <EDi|toR> after".into(),
13547 completion_label: "editor",
13548 completion_text: "editor",
13549 expected_with_insert_mode: "before editorˇtoR after".into(),
13550 expected_with_replace_mode: "before editorˇ after".into(),
13551 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13552 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13553 },
13554 ];
13555
13556 for run in runs {
13557 let run_variations = [
13558 (LspInsertMode::Insert, run.expected_with_insert_mode),
13559 (LspInsertMode::Replace, run.expected_with_replace_mode),
13560 (
13561 LspInsertMode::ReplaceSubsequence,
13562 run.expected_with_replace_subsequence_mode,
13563 ),
13564 (
13565 LspInsertMode::ReplaceSuffix,
13566 run.expected_with_replace_suffix_mode,
13567 ),
13568 ];
13569
13570 for (lsp_insert_mode, expected_text) in run_variations {
13571 eprintln!(
13572 "run = {:?}, mode = {lsp_insert_mode:.?}",
13573 run.run_description,
13574 );
13575
13576 update_test_language_settings(&mut cx, |settings| {
13577 settings.defaults.completions = Some(CompletionSettingsContent {
13578 lsp_insert_mode: Some(lsp_insert_mode),
13579 words: Some(WordsCompletionMode::Disabled),
13580 words_min_length: Some(0),
13581 ..Default::default()
13582 });
13583 });
13584
13585 cx.set_state(&run.initial_state);
13586 cx.update_editor(|editor, window, cx| {
13587 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13588 });
13589
13590 let counter = Arc::new(AtomicUsize::new(0));
13591 handle_completion_request_with_insert_and_replace(
13592 &mut cx,
13593 &run.buffer_marked_text,
13594 vec![(run.completion_label, run.completion_text)],
13595 counter.clone(),
13596 )
13597 .await;
13598 cx.condition(|editor, _| editor.context_menu_visible())
13599 .await;
13600 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13601
13602 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13603 editor
13604 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13605 .unwrap()
13606 });
13607 cx.assert_editor_state(&expected_text);
13608 handle_resolve_completion_request(&mut cx, None).await;
13609 apply_additional_edits.await.unwrap();
13610 }
13611 }
13612}
13613
13614#[gpui::test]
13615async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13616 init_test(cx, |_| {});
13617 let mut cx = EditorLspTestContext::new_rust(
13618 lsp::ServerCapabilities {
13619 completion_provider: Some(lsp::CompletionOptions {
13620 resolve_provider: Some(true),
13621 ..Default::default()
13622 }),
13623 ..Default::default()
13624 },
13625 cx,
13626 )
13627 .await;
13628
13629 let initial_state = "SubˇError";
13630 let buffer_marked_text = "<Sub|Error>";
13631 let completion_text = "SubscriptionError";
13632 let expected_with_insert_mode = "SubscriptionErrorˇError";
13633 let expected_with_replace_mode = "SubscriptionErrorˇ";
13634
13635 update_test_language_settings(&mut cx, |settings| {
13636 settings.defaults.completions = Some(CompletionSettingsContent {
13637 words: Some(WordsCompletionMode::Disabled),
13638 words_min_length: Some(0),
13639 // set the opposite here to ensure that the action is overriding the default behavior
13640 lsp_insert_mode: Some(LspInsertMode::Insert),
13641 ..Default::default()
13642 });
13643 });
13644
13645 cx.set_state(initial_state);
13646 cx.update_editor(|editor, window, cx| {
13647 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13648 });
13649
13650 let counter = Arc::new(AtomicUsize::new(0));
13651 handle_completion_request_with_insert_and_replace(
13652 &mut cx,
13653 buffer_marked_text,
13654 vec![(completion_text, completion_text)],
13655 counter.clone(),
13656 )
13657 .await;
13658 cx.condition(|editor, _| editor.context_menu_visible())
13659 .await;
13660 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13661
13662 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13663 editor
13664 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13665 .unwrap()
13666 });
13667 cx.assert_editor_state(expected_with_replace_mode);
13668 handle_resolve_completion_request(&mut cx, None).await;
13669 apply_additional_edits.await.unwrap();
13670
13671 update_test_language_settings(&mut cx, |settings| {
13672 settings.defaults.completions = Some(CompletionSettingsContent {
13673 words: Some(WordsCompletionMode::Disabled),
13674 words_min_length: Some(0),
13675 // set the opposite here to ensure that the action is overriding the default behavior
13676 lsp_insert_mode: Some(LspInsertMode::Replace),
13677 ..Default::default()
13678 });
13679 });
13680
13681 cx.set_state(initial_state);
13682 cx.update_editor(|editor, window, cx| {
13683 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13684 });
13685 handle_completion_request_with_insert_and_replace(
13686 &mut cx,
13687 buffer_marked_text,
13688 vec![(completion_text, completion_text)],
13689 counter.clone(),
13690 )
13691 .await;
13692 cx.condition(|editor, _| editor.context_menu_visible())
13693 .await;
13694 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13695
13696 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13697 editor
13698 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13699 .unwrap()
13700 });
13701 cx.assert_editor_state(expected_with_insert_mode);
13702 handle_resolve_completion_request(&mut cx, None).await;
13703 apply_additional_edits.await.unwrap();
13704}
13705
13706#[gpui::test]
13707async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13708 init_test(cx, |_| {});
13709 let mut cx = EditorLspTestContext::new_rust(
13710 lsp::ServerCapabilities {
13711 completion_provider: Some(lsp::CompletionOptions {
13712 resolve_provider: Some(true),
13713 ..Default::default()
13714 }),
13715 ..Default::default()
13716 },
13717 cx,
13718 )
13719 .await;
13720
13721 // scenario: surrounding text matches completion text
13722 let completion_text = "to_offset";
13723 let initial_state = indoc! {"
13724 1. buf.to_offˇsuffix
13725 2. buf.to_offˇsuf
13726 3. buf.to_offˇfix
13727 4. buf.to_offˇ
13728 5. into_offˇensive
13729 6. ˇsuffix
13730 7. let ˇ //
13731 8. aaˇzz
13732 9. buf.to_off«zzzzzˇ»suffix
13733 10. buf.«ˇzzzzz»suffix
13734 11. to_off«ˇzzzzz»
13735
13736 buf.to_offˇsuffix // newest cursor
13737 "};
13738 let completion_marked_buffer = indoc! {"
13739 1. buf.to_offsuffix
13740 2. buf.to_offsuf
13741 3. buf.to_offfix
13742 4. buf.to_off
13743 5. into_offensive
13744 6. suffix
13745 7. let //
13746 8. aazz
13747 9. buf.to_offzzzzzsuffix
13748 10. buf.zzzzzsuffix
13749 11. to_offzzzzz
13750
13751 buf.<to_off|suffix> // newest cursor
13752 "};
13753 let expected = indoc! {"
13754 1. buf.to_offsetˇ
13755 2. buf.to_offsetˇsuf
13756 3. buf.to_offsetˇfix
13757 4. buf.to_offsetˇ
13758 5. into_offsetˇensive
13759 6. to_offsetˇsuffix
13760 7. let to_offsetˇ //
13761 8. aato_offsetˇzz
13762 9. buf.to_offsetˇ
13763 10. buf.to_offsetˇsuffix
13764 11. to_offsetˇ
13765
13766 buf.to_offsetˇ // newest cursor
13767 "};
13768 cx.set_state(initial_state);
13769 cx.update_editor(|editor, window, cx| {
13770 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13771 });
13772 handle_completion_request_with_insert_and_replace(
13773 &mut cx,
13774 completion_marked_buffer,
13775 vec![(completion_text, completion_text)],
13776 Arc::new(AtomicUsize::new(0)),
13777 )
13778 .await;
13779 cx.condition(|editor, _| editor.context_menu_visible())
13780 .await;
13781 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13782 editor
13783 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13784 .unwrap()
13785 });
13786 cx.assert_editor_state(expected);
13787 handle_resolve_completion_request(&mut cx, None).await;
13788 apply_additional_edits.await.unwrap();
13789
13790 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13791 let completion_text = "foo_and_bar";
13792 let initial_state = indoc! {"
13793 1. ooanbˇ
13794 2. zooanbˇ
13795 3. ooanbˇz
13796 4. zooanbˇz
13797 5. ooanˇ
13798 6. oanbˇ
13799
13800 ooanbˇ
13801 "};
13802 let completion_marked_buffer = indoc! {"
13803 1. ooanb
13804 2. zooanb
13805 3. ooanbz
13806 4. zooanbz
13807 5. ooan
13808 6. oanb
13809
13810 <ooanb|>
13811 "};
13812 let expected = indoc! {"
13813 1. foo_and_barˇ
13814 2. zfoo_and_barˇ
13815 3. foo_and_barˇz
13816 4. zfoo_and_barˇz
13817 5. ooanfoo_and_barˇ
13818 6. oanbfoo_and_barˇ
13819
13820 foo_and_barˇ
13821 "};
13822 cx.set_state(initial_state);
13823 cx.update_editor(|editor, window, cx| {
13824 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13825 });
13826 handle_completion_request_with_insert_and_replace(
13827 &mut cx,
13828 completion_marked_buffer,
13829 vec![(completion_text, completion_text)],
13830 Arc::new(AtomicUsize::new(0)),
13831 )
13832 .await;
13833 cx.condition(|editor, _| editor.context_menu_visible())
13834 .await;
13835 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13836 editor
13837 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13838 .unwrap()
13839 });
13840 cx.assert_editor_state(expected);
13841 handle_resolve_completion_request(&mut cx, None).await;
13842 apply_additional_edits.await.unwrap();
13843
13844 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13845 // (expects the same as if it was inserted at the end)
13846 let completion_text = "foo_and_bar";
13847 let initial_state = indoc! {"
13848 1. ooˇanb
13849 2. zooˇanb
13850 3. ooˇanbz
13851 4. zooˇanbz
13852
13853 ooˇanb
13854 "};
13855 let completion_marked_buffer = indoc! {"
13856 1. ooanb
13857 2. zooanb
13858 3. ooanbz
13859 4. zooanbz
13860
13861 <oo|anb>
13862 "};
13863 let expected = indoc! {"
13864 1. foo_and_barˇ
13865 2. zfoo_and_barˇ
13866 3. foo_and_barˇz
13867 4. zfoo_and_barˇz
13868
13869 foo_and_barˇ
13870 "};
13871 cx.set_state(initial_state);
13872 cx.update_editor(|editor, window, cx| {
13873 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13874 });
13875 handle_completion_request_with_insert_and_replace(
13876 &mut cx,
13877 completion_marked_buffer,
13878 vec![(completion_text, completion_text)],
13879 Arc::new(AtomicUsize::new(0)),
13880 )
13881 .await;
13882 cx.condition(|editor, _| editor.context_menu_visible())
13883 .await;
13884 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13885 editor
13886 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13887 .unwrap()
13888 });
13889 cx.assert_editor_state(expected);
13890 handle_resolve_completion_request(&mut cx, None).await;
13891 apply_additional_edits.await.unwrap();
13892}
13893
13894// This used to crash
13895#[gpui::test]
13896async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13897 init_test(cx, |_| {});
13898
13899 let buffer_text = indoc! {"
13900 fn main() {
13901 10.satu;
13902
13903 //
13904 // separate cursors so they open in different excerpts (manually reproducible)
13905 //
13906
13907 10.satu20;
13908 }
13909 "};
13910 let multibuffer_text_with_selections = indoc! {"
13911 fn main() {
13912 10.satuˇ;
13913
13914 //
13915
13916 //
13917
13918 10.satuˇ20;
13919 }
13920 "};
13921 let expected_multibuffer = indoc! {"
13922 fn main() {
13923 10.saturating_sub()ˇ;
13924
13925 //
13926
13927 //
13928
13929 10.saturating_sub()ˇ;
13930 }
13931 "};
13932
13933 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13934 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13935
13936 let fs = FakeFs::new(cx.executor());
13937 fs.insert_tree(
13938 path!("/a"),
13939 json!({
13940 "main.rs": buffer_text,
13941 }),
13942 )
13943 .await;
13944
13945 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13946 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13947 language_registry.add(rust_lang());
13948 let mut fake_servers = language_registry.register_fake_lsp(
13949 "Rust",
13950 FakeLspAdapter {
13951 capabilities: lsp::ServerCapabilities {
13952 completion_provider: Some(lsp::CompletionOptions {
13953 resolve_provider: None,
13954 ..lsp::CompletionOptions::default()
13955 }),
13956 ..lsp::ServerCapabilities::default()
13957 },
13958 ..FakeLspAdapter::default()
13959 },
13960 );
13961 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13962 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13963 let buffer = project
13964 .update(cx, |project, cx| {
13965 project.open_local_buffer(path!("/a/main.rs"), cx)
13966 })
13967 .await
13968 .unwrap();
13969
13970 let multi_buffer = cx.new(|cx| {
13971 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13972 multi_buffer.push_excerpts(
13973 buffer.clone(),
13974 [ExcerptRange::new(0..first_excerpt_end)],
13975 cx,
13976 );
13977 multi_buffer.push_excerpts(
13978 buffer.clone(),
13979 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13980 cx,
13981 );
13982 multi_buffer
13983 });
13984
13985 let editor = workspace
13986 .update(cx, |_, window, cx| {
13987 cx.new(|cx| {
13988 Editor::new(
13989 EditorMode::Full {
13990 scale_ui_elements_with_buffer_font_size: false,
13991 show_active_line_background: false,
13992 sized_by_content: false,
13993 },
13994 multi_buffer.clone(),
13995 Some(project.clone()),
13996 window,
13997 cx,
13998 )
13999 })
14000 })
14001 .unwrap();
14002
14003 let pane = workspace
14004 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14005 .unwrap();
14006 pane.update_in(cx, |pane, window, cx| {
14007 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14008 });
14009
14010 let fake_server = fake_servers.next().await.unwrap();
14011
14012 editor.update_in(cx, |editor, window, cx| {
14013 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14014 s.select_ranges([
14015 Point::new(1, 11)..Point::new(1, 11),
14016 Point::new(7, 11)..Point::new(7, 11),
14017 ])
14018 });
14019
14020 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14021 });
14022
14023 editor.update_in(cx, |editor, window, cx| {
14024 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14025 });
14026
14027 fake_server
14028 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14029 let completion_item = lsp::CompletionItem {
14030 label: "saturating_sub()".into(),
14031 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14032 lsp::InsertReplaceEdit {
14033 new_text: "saturating_sub()".to_owned(),
14034 insert: lsp::Range::new(
14035 lsp::Position::new(7, 7),
14036 lsp::Position::new(7, 11),
14037 ),
14038 replace: lsp::Range::new(
14039 lsp::Position::new(7, 7),
14040 lsp::Position::new(7, 13),
14041 ),
14042 },
14043 )),
14044 ..lsp::CompletionItem::default()
14045 };
14046
14047 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14048 })
14049 .next()
14050 .await
14051 .unwrap();
14052
14053 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14054 .await;
14055
14056 editor
14057 .update_in(cx, |editor, window, cx| {
14058 editor
14059 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14060 .unwrap()
14061 })
14062 .await
14063 .unwrap();
14064
14065 editor.update(cx, |editor, cx| {
14066 assert_text_with_selections(editor, expected_multibuffer, cx);
14067 })
14068}
14069
14070#[gpui::test]
14071async fn test_completion(cx: &mut TestAppContext) {
14072 init_test(cx, |_| {});
14073
14074 let mut cx = EditorLspTestContext::new_rust(
14075 lsp::ServerCapabilities {
14076 completion_provider: Some(lsp::CompletionOptions {
14077 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14078 resolve_provider: Some(true),
14079 ..Default::default()
14080 }),
14081 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14082 ..Default::default()
14083 },
14084 cx,
14085 )
14086 .await;
14087 let counter = Arc::new(AtomicUsize::new(0));
14088
14089 cx.set_state(indoc! {"
14090 oneˇ
14091 two
14092 three
14093 "});
14094 cx.simulate_keystroke(".");
14095 handle_completion_request(
14096 indoc! {"
14097 one.|<>
14098 two
14099 three
14100 "},
14101 vec!["first_completion", "second_completion"],
14102 true,
14103 counter.clone(),
14104 &mut cx,
14105 )
14106 .await;
14107 cx.condition(|editor, _| editor.context_menu_visible())
14108 .await;
14109 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14110
14111 let _handler = handle_signature_help_request(
14112 &mut cx,
14113 lsp::SignatureHelp {
14114 signatures: vec![lsp::SignatureInformation {
14115 label: "test signature".to_string(),
14116 documentation: None,
14117 parameters: Some(vec![lsp::ParameterInformation {
14118 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14119 documentation: None,
14120 }]),
14121 active_parameter: None,
14122 }],
14123 active_signature: None,
14124 active_parameter: None,
14125 },
14126 );
14127 cx.update_editor(|editor, window, cx| {
14128 assert!(
14129 !editor.signature_help_state.is_shown(),
14130 "No signature help was called for"
14131 );
14132 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14133 });
14134 cx.run_until_parked();
14135 cx.update_editor(|editor, _, _| {
14136 assert!(
14137 !editor.signature_help_state.is_shown(),
14138 "No signature help should be shown when completions menu is open"
14139 );
14140 });
14141
14142 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14143 editor.context_menu_next(&Default::default(), window, cx);
14144 editor
14145 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14146 .unwrap()
14147 });
14148 cx.assert_editor_state(indoc! {"
14149 one.second_completionˇ
14150 two
14151 three
14152 "});
14153
14154 handle_resolve_completion_request(
14155 &mut cx,
14156 Some(vec![
14157 (
14158 //This overlaps with the primary completion edit which is
14159 //misbehavior from the LSP spec, test that we filter it out
14160 indoc! {"
14161 one.second_ˇcompletion
14162 two
14163 threeˇ
14164 "},
14165 "overlapping additional edit",
14166 ),
14167 (
14168 indoc! {"
14169 one.second_completion
14170 two
14171 threeˇ
14172 "},
14173 "\nadditional edit",
14174 ),
14175 ]),
14176 )
14177 .await;
14178 apply_additional_edits.await.unwrap();
14179 cx.assert_editor_state(indoc! {"
14180 one.second_completionˇ
14181 two
14182 three
14183 additional edit
14184 "});
14185
14186 cx.set_state(indoc! {"
14187 one.second_completion
14188 twoˇ
14189 threeˇ
14190 additional edit
14191 "});
14192 cx.simulate_keystroke(" ");
14193 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14194 cx.simulate_keystroke("s");
14195 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14196
14197 cx.assert_editor_state(indoc! {"
14198 one.second_completion
14199 two sˇ
14200 three sˇ
14201 additional edit
14202 "});
14203 handle_completion_request(
14204 indoc! {"
14205 one.second_completion
14206 two s
14207 three <s|>
14208 additional edit
14209 "},
14210 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14211 true,
14212 counter.clone(),
14213 &mut cx,
14214 )
14215 .await;
14216 cx.condition(|editor, _| editor.context_menu_visible())
14217 .await;
14218 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14219
14220 cx.simulate_keystroke("i");
14221
14222 handle_completion_request(
14223 indoc! {"
14224 one.second_completion
14225 two si
14226 three <si|>
14227 additional edit
14228 "},
14229 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14230 true,
14231 counter.clone(),
14232 &mut cx,
14233 )
14234 .await;
14235 cx.condition(|editor, _| editor.context_menu_visible())
14236 .await;
14237 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14238
14239 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14240 editor
14241 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14242 .unwrap()
14243 });
14244 cx.assert_editor_state(indoc! {"
14245 one.second_completion
14246 two sixth_completionˇ
14247 three sixth_completionˇ
14248 additional edit
14249 "});
14250
14251 apply_additional_edits.await.unwrap();
14252
14253 update_test_language_settings(&mut cx, |settings| {
14254 settings.defaults.show_completions_on_input = Some(false);
14255 });
14256 cx.set_state("editorˇ");
14257 cx.simulate_keystroke(".");
14258 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14259 cx.simulate_keystrokes("c l o");
14260 cx.assert_editor_state("editor.cloˇ");
14261 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14262 cx.update_editor(|editor, window, cx| {
14263 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14264 });
14265 handle_completion_request(
14266 "editor.<clo|>",
14267 vec!["close", "clobber"],
14268 true,
14269 counter.clone(),
14270 &mut cx,
14271 )
14272 .await;
14273 cx.condition(|editor, _| editor.context_menu_visible())
14274 .await;
14275 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14276
14277 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14278 editor
14279 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14280 .unwrap()
14281 });
14282 cx.assert_editor_state("editor.clobberˇ");
14283 handle_resolve_completion_request(&mut cx, None).await;
14284 apply_additional_edits.await.unwrap();
14285}
14286
14287#[gpui::test]
14288async fn test_completion_reuse(cx: &mut TestAppContext) {
14289 init_test(cx, |_| {});
14290
14291 let mut cx = EditorLspTestContext::new_rust(
14292 lsp::ServerCapabilities {
14293 completion_provider: Some(lsp::CompletionOptions {
14294 trigger_characters: Some(vec![".".to_string()]),
14295 ..Default::default()
14296 }),
14297 ..Default::default()
14298 },
14299 cx,
14300 )
14301 .await;
14302
14303 let counter = Arc::new(AtomicUsize::new(0));
14304 cx.set_state("objˇ");
14305 cx.simulate_keystroke(".");
14306
14307 // Initial completion request returns complete results
14308 let is_incomplete = false;
14309 handle_completion_request(
14310 "obj.|<>",
14311 vec!["a", "ab", "abc"],
14312 is_incomplete,
14313 counter.clone(),
14314 &mut cx,
14315 )
14316 .await;
14317 cx.run_until_parked();
14318 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14319 cx.assert_editor_state("obj.ˇ");
14320 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14321
14322 // Type "a" - filters existing completions
14323 cx.simulate_keystroke("a");
14324 cx.run_until_parked();
14325 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14326 cx.assert_editor_state("obj.aˇ");
14327 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14328
14329 // Type "b" - filters existing completions
14330 cx.simulate_keystroke("b");
14331 cx.run_until_parked();
14332 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14333 cx.assert_editor_state("obj.abˇ");
14334 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14335
14336 // Type "c" - filters existing completions
14337 cx.simulate_keystroke("c");
14338 cx.run_until_parked();
14339 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14340 cx.assert_editor_state("obj.abcˇ");
14341 check_displayed_completions(vec!["abc"], &mut cx);
14342
14343 // Backspace to delete "c" - filters existing completions
14344 cx.update_editor(|editor, window, cx| {
14345 editor.backspace(&Backspace, window, cx);
14346 });
14347 cx.run_until_parked();
14348 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14349 cx.assert_editor_state("obj.abˇ");
14350 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14351
14352 // Moving cursor to the left dismisses menu.
14353 cx.update_editor(|editor, window, cx| {
14354 editor.move_left(&MoveLeft, window, cx);
14355 });
14356 cx.run_until_parked();
14357 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14358 cx.assert_editor_state("obj.aˇb");
14359 cx.update_editor(|editor, _, _| {
14360 assert_eq!(editor.context_menu_visible(), false);
14361 });
14362
14363 // Type "b" - new request
14364 cx.simulate_keystroke("b");
14365 let is_incomplete = false;
14366 handle_completion_request(
14367 "obj.<ab|>a",
14368 vec!["ab", "abc"],
14369 is_incomplete,
14370 counter.clone(),
14371 &mut cx,
14372 )
14373 .await;
14374 cx.run_until_parked();
14375 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14376 cx.assert_editor_state("obj.abˇb");
14377 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14378
14379 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14380 cx.update_editor(|editor, window, cx| {
14381 editor.backspace(&Backspace, window, cx);
14382 });
14383 let is_incomplete = false;
14384 handle_completion_request(
14385 "obj.<a|>b",
14386 vec!["a", "ab", "abc"],
14387 is_incomplete,
14388 counter.clone(),
14389 &mut cx,
14390 )
14391 .await;
14392 cx.run_until_parked();
14393 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14394 cx.assert_editor_state("obj.aˇb");
14395 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14396
14397 // Backspace to delete "a" - dismisses menu.
14398 cx.update_editor(|editor, window, cx| {
14399 editor.backspace(&Backspace, window, cx);
14400 });
14401 cx.run_until_parked();
14402 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14403 cx.assert_editor_state("obj.ˇb");
14404 cx.update_editor(|editor, _, _| {
14405 assert_eq!(editor.context_menu_visible(), false);
14406 });
14407}
14408
14409#[gpui::test]
14410async fn test_word_completion(cx: &mut TestAppContext) {
14411 let lsp_fetch_timeout_ms = 10;
14412 init_test(cx, |language_settings| {
14413 language_settings.defaults.completions = Some(CompletionSettingsContent {
14414 words_min_length: Some(0),
14415 lsp_fetch_timeout_ms: Some(10),
14416 lsp_insert_mode: Some(LspInsertMode::Insert),
14417 ..Default::default()
14418 });
14419 });
14420
14421 let mut cx = EditorLspTestContext::new_rust(
14422 lsp::ServerCapabilities {
14423 completion_provider: Some(lsp::CompletionOptions {
14424 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14425 ..lsp::CompletionOptions::default()
14426 }),
14427 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14428 ..lsp::ServerCapabilities::default()
14429 },
14430 cx,
14431 )
14432 .await;
14433
14434 let throttle_completions = Arc::new(AtomicBool::new(false));
14435
14436 let lsp_throttle_completions = throttle_completions.clone();
14437 let _completion_requests_handler =
14438 cx.lsp
14439 .server
14440 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14441 let lsp_throttle_completions = lsp_throttle_completions.clone();
14442 let cx = cx.clone();
14443 async move {
14444 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14445 cx.background_executor()
14446 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14447 .await;
14448 }
14449 Ok(Some(lsp::CompletionResponse::Array(vec![
14450 lsp::CompletionItem {
14451 label: "first".into(),
14452 ..lsp::CompletionItem::default()
14453 },
14454 lsp::CompletionItem {
14455 label: "last".into(),
14456 ..lsp::CompletionItem::default()
14457 },
14458 ])))
14459 }
14460 });
14461
14462 cx.set_state(indoc! {"
14463 oneˇ
14464 two
14465 three
14466 "});
14467 cx.simulate_keystroke(".");
14468 cx.executor().run_until_parked();
14469 cx.condition(|editor, _| editor.context_menu_visible())
14470 .await;
14471 cx.update_editor(|editor, window, cx| {
14472 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14473 {
14474 assert_eq!(
14475 completion_menu_entries(menu),
14476 &["first", "last"],
14477 "When LSP server is fast to reply, no fallback word completions are used"
14478 );
14479 } else {
14480 panic!("expected completion menu to be open");
14481 }
14482 editor.cancel(&Cancel, window, cx);
14483 });
14484 cx.executor().run_until_parked();
14485 cx.condition(|editor, _| !editor.context_menu_visible())
14486 .await;
14487
14488 throttle_completions.store(true, atomic::Ordering::Release);
14489 cx.simulate_keystroke(".");
14490 cx.executor()
14491 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14492 cx.executor().run_until_parked();
14493 cx.condition(|editor, _| editor.context_menu_visible())
14494 .await;
14495 cx.update_editor(|editor, _, _| {
14496 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14497 {
14498 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14499 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14500 } else {
14501 panic!("expected completion menu to be open");
14502 }
14503 });
14504}
14505
14506#[gpui::test]
14507async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14508 init_test(cx, |language_settings| {
14509 language_settings.defaults.completions = Some(CompletionSettingsContent {
14510 words: Some(WordsCompletionMode::Enabled),
14511 words_min_length: Some(0),
14512 lsp_insert_mode: Some(LspInsertMode::Insert),
14513 ..Default::default()
14514 });
14515 });
14516
14517 let mut cx = EditorLspTestContext::new_rust(
14518 lsp::ServerCapabilities {
14519 completion_provider: Some(lsp::CompletionOptions {
14520 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14521 ..lsp::CompletionOptions::default()
14522 }),
14523 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14524 ..lsp::ServerCapabilities::default()
14525 },
14526 cx,
14527 )
14528 .await;
14529
14530 let _completion_requests_handler =
14531 cx.lsp
14532 .server
14533 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14534 Ok(Some(lsp::CompletionResponse::Array(vec![
14535 lsp::CompletionItem {
14536 label: "first".into(),
14537 ..lsp::CompletionItem::default()
14538 },
14539 lsp::CompletionItem {
14540 label: "last".into(),
14541 ..lsp::CompletionItem::default()
14542 },
14543 ])))
14544 });
14545
14546 cx.set_state(indoc! {"ˇ
14547 first
14548 last
14549 second
14550 "});
14551 cx.simulate_keystroke(".");
14552 cx.executor().run_until_parked();
14553 cx.condition(|editor, _| editor.context_menu_visible())
14554 .await;
14555 cx.update_editor(|editor, _, _| {
14556 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14557 {
14558 assert_eq!(
14559 completion_menu_entries(menu),
14560 &["first", "last", "second"],
14561 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14562 );
14563 } else {
14564 panic!("expected completion menu to be open");
14565 }
14566 });
14567}
14568
14569#[gpui::test]
14570async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14571 init_test(cx, |language_settings| {
14572 language_settings.defaults.completions = Some(CompletionSettingsContent {
14573 words: Some(WordsCompletionMode::Disabled),
14574 words_min_length: Some(0),
14575 lsp_insert_mode: Some(LspInsertMode::Insert),
14576 ..Default::default()
14577 });
14578 });
14579
14580 let mut cx = EditorLspTestContext::new_rust(
14581 lsp::ServerCapabilities {
14582 completion_provider: Some(lsp::CompletionOptions {
14583 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14584 ..lsp::CompletionOptions::default()
14585 }),
14586 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14587 ..lsp::ServerCapabilities::default()
14588 },
14589 cx,
14590 )
14591 .await;
14592
14593 let _completion_requests_handler =
14594 cx.lsp
14595 .server
14596 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14597 panic!("LSP completions should not be queried when dealing with word completions")
14598 });
14599
14600 cx.set_state(indoc! {"ˇ
14601 first
14602 last
14603 second
14604 "});
14605 cx.update_editor(|editor, window, cx| {
14606 editor.show_word_completions(&ShowWordCompletions, window, cx);
14607 });
14608 cx.executor().run_until_parked();
14609 cx.condition(|editor, _| editor.context_menu_visible())
14610 .await;
14611 cx.update_editor(|editor, _, _| {
14612 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14613 {
14614 assert_eq!(
14615 completion_menu_entries(menu),
14616 &["first", "last", "second"],
14617 "`ShowWordCompletions` action should show word completions"
14618 );
14619 } else {
14620 panic!("expected completion menu to be open");
14621 }
14622 });
14623
14624 cx.simulate_keystroke("l");
14625 cx.executor().run_until_parked();
14626 cx.condition(|editor, _| editor.context_menu_visible())
14627 .await;
14628 cx.update_editor(|editor, _, _| {
14629 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14630 {
14631 assert_eq!(
14632 completion_menu_entries(menu),
14633 &["last"],
14634 "After showing word completions, further editing should filter them and not query the LSP"
14635 );
14636 } else {
14637 panic!("expected completion menu to be open");
14638 }
14639 });
14640}
14641
14642#[gpui::test]
14643async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14644 init_test(cx, |language_settings| {
14645 language_settings.defaults.completions = Some(CompletionSettingsContent {
14646 words_min_length: Some(0),
14647 lsp: Some(false),
14648 lsp_insert_mode: Some(LspInsertMode::Insert),
14649 ..Default::default()
14650 });
14651 });
14652
14653 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14654
14655 cx.set_state(indoc! {"ˇ
14656 0_usize
14657 let
14658 33
14659 4.5f32
14660 "});
14661 cx.update_editor(|editor, window, cx| {
14662 editor.show_completions(&ShowCompletions::default(), window, cx);
14663 });
14664 cx.executor().run_until_parked();
14665 cx.condition(|editor, _| editor.context_menu_visible())
14666 .await;
14667 cx.update_editor(|editor, window, cx| {
14668 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14669 {
14670 assert_eq!(
14671 completion_menu_entries(menu),
14672 &["let"],
14673 "With no digits in the completion query, no digits should be in the word completions"
14674 );
14675 } else {
14676 panic!("expected completion menu to be open");
14677 }
14678 editor.cancel(&Cancel, window, cx);
14679 });
14680
14681 cx.set_state(indoc! {"3ˇ
14682 0_usize
14683 let
14684 3
14685 33.35f32
14686 "});
14687 cx.update_editor(|editor, window, cx| {
14688 editor.show_completions(&ShowCompletions::default(), window, cx);
14689 });
14690 cx.executor().run_until_parked();
14691 cx.condition(|editor, _| editor.context_menu_visible())
14692 .await;
14693 cx.update_editor(|editor, _, _| {
14694 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14695 {
14696 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14697 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14698 } else {
14699 panic!("expected completion menu to be open");
14700 }
14701 });
14702}
14703
14704#[gpui::test]
14705async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14706 init_test(cx, |language_settings| {
14707 language_settings.defaults.completions = Some(CompletionSettingsContent {
14708 words: Some(WordsCompletionMode::Enabled),
14709 words_min_length: Some(3),
14710 lsp_insert_mode: Some(LspInsertMode::Insert),
14711 ..Default::default()
14712 });
14713 });
14714
14715 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14716 cx.set_state(indoc! {"ˇ
14717 wow
14718 wowen
14719 wowser
14720 "});
14721 cx.simulate_keystroke("w");
14722 cx.executor().run_until_parked();
14723 cx.update_editor(|editor, _, _| {
14724 if editor.context_menu.borrow_mut().is_some() {
14725 panic!(
14726 "expected completion menu to be hidden, as words completion threshold is not met"
14727 );
14728 }
14729 });
14730
14731 cx.update_editor(|editor, window, cx| {
14732 editor.show_word_completions(&ShowWordCompletions, window, cx);
14733 });
14734 cx.executor().run_until_parked();
14735 cx.update_editor(|editor, window, cx| {
14736 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14737 {
14738 assert_eq!(completion_menu_entries(menu), &["wowser", "wowen", "wow"], "Even though the threshold is not met, invoking word completions with an action should provide the completions");
14739 } else {
14740 panic!("expected completion menu to be open after the word completions are called with an action");
14741 }
14742
14743 editor.cancel(&Cancel, window, cx);
14744 });
14745 cx.update_editor(|editor, _, _| {
14746 if editor.context_menu.borrow_mut().is_some() {
14747 panic!("expected completion menu to be hidden after canceling");
14748 }
14749 });
14750
14751 cx.simulate_keystroke("o");
14752 cx.executor().run_until_parked();
14753 cx.update_editor(|editor, _, _| {
14754 if editor.context_menu.borrow_mut().is_some() {
14755 panic!(
14756 "expected completion menu to be hidden, as words completion threshold is not met still"
14757 );
14758 }
14759 });
14760
14761 cx.simulate_keystroke("w");
14762 cx.executor().run_until_parked();
14763 cx.update_editor(|editor, _, _| {
14764 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14765 {
14766 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14767 } else {
14768 panic!("expected completion menu to be open after the word completions threshold is met");
14769 }
14770 });
14771}
14772
14773#[gpui::test]
14774async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14775 init_test(cx, |language_settings| {
14776 language_settings.defaults.completions = Some(CompletionSettingsContent {
14777 words: Some(WordsCompletionMode::Enabled),
14778 words_min_length: Some(0),
14779 lsp_insert_mode: Some(LspInsertMode::Insert),
14780 ..Default::default()
14781 });
14782 });
14783
14784 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14785 cx.update_editor(|editor, _, _| {
14786 editor.disable_word_completions();
14787 });
14788 cx.set_state(indoc! {"ˇ
14789 wow
14790 wowen
14791 wowser
14792 "});
14793 cx.simulate_keystroke("w");
14794 cx.executor().run_until_parked();
14795 cx.update_editor(|editor, _, _| {
14796 if editor.context_menu.borrow_mut().is_some() {
14797 panic!(
14798 "expected completion menu to be hidden, as words completion are disabled for this editor"
14799 );
14800 }
14801 });
14802
14803 cx.update_editor(|editor, window, cx| {
14804 editor.show_word_completions(&ShowWordCompletions, window, cx);
14805 });
14806 cx.executor().run_until_parked();
14807 cx.update_editor(|editor, _, _| {
14808 if editor.context_menu.borrow_mut().is_some() {
14809 panic!(
14810 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14811 );
14812 }
14813 });
14814}
14815
14816fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14817 let position = || lsp::Position {
14818 line: params.text_document_position.position.line,
14819 character: params.text_document_position.position.character,
14820 };
14821 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14822 range: lsp::Range {
14823 start: position(),
14824 end: position(),
14825 },
14826 new_text: text.to_string(),
14827 }))
14828}
14829
14830#[gpui::test]
14831async fn test_multiline_completion(cx: &mut TestAppContext) {
14832 init_test(cx, |_| {});
14833
14834 let fs = FakeFs::new(cx.executor());
14835 fs.insert_tree(
14836 path!("/a"),
14837 json!({
14838 "main.ts": "a",
14839 }),
14840 )
14841 .await;
14842
14843 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14844 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14845 let typescript_language = Arc::new(Language::new(
14846 LanguageConfig {
14847 name: "TypeScript".into(),
14848 matcher: LanguageMatcher {
14849 path_suffixes: vec!["ts".to_string()],
14850 ..LanguageMatcher::default()
14851 },
14852 line_comments: vec!["// ".into()],
14853 ..LanguageConfig::default()
14854 },
14855 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14856 ));
14857 language_registry.add(typescript_language.clone());
14858 let mut fake_servers = language_registry.register_fake_lsp(
14859 "TypeScript",
14860 FakeLspAdapter {
14861 capabilities: lsp::ServerCapabilities {
14862 completion_provider: Some(lsp::CompletionOptions {
14863 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14864 ..lsp::CompletionOptions::default()
14865 }),
14866 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14867 ..lsp::ServerCapabilities::default()
14868 },
14869 // Emulate vtsls label generation
14870 label_for_completion: Some(Box::new(|item, _| {
14871 let text = if let Some(description) = item
14872 .label_details
14873 .as_ref()
14874 .and_then(|label_details| label_details.description.as_ref())
14875 {
14876 format!("{} {}", item.label, description)
14877 } else if let Some(detail) = &item.detail {
14878 format!("{} {}", item.label, detail)
14879 } else {
14880 item.label.clone()
14881 };
14882 Some(language::CodeLabel::plain(text, None))
14883 })),
14884 ..FakeLspAdapter::default()
14885 },
14886 );
14887 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14888 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14889 let worktree_id = workspace
14890 .update(cx, |workspace, _window, cx| {
14891 workspace.project().update(cx, |project, cx| {
14892 project.worktrees(cx).next().unwrap().read(cx).id()
14893 })
14894 })
14895 .unwrap();
14896 let _buffer = project
14897 .update(cx, |project, cx| {
14898 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14899 })
14900 .await
14901 .unwrap();
14902 let editor = workspace
14903 .update(cx, |workspace, window, cx| {
14904 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
14905 })
14906 .unwrap()
14907 .await
14908 .unwrap()
14909 .downcast::<Editor>()
14910 .unwrap();
14911 let fake_server = fake_servers.next().await.unwrap();
14912
14913 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14914 let multiline_label_2 = "a\nb\nc\n";
14915 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14916 let multiline_description = "d\ne\nf\n";
14917 let multiline_detail_2 = "g\nh\ni\n";
14918
14919 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14920 move |params, _| async move {
14921 Ok(Some(lsp::CompletionResponse::Array(vec![
14922 lsp::CompletionItem {
14923 label: multiline_label.to_string(),
14924 text_edit: gen_text_edit(¶ms, "new_text_1"),
14925 ..lsp::CompletionItem::default()
14926 },
14927 lsp::CompletionItem {
14928 label: "single line label 1".to_string(),
14929 detail: Some(multiline_detail.to_string()),
14930 text_edit: gen_text_edit(¶ms, "new_text_2"),
14931 ..lsp::CompletionItem::default()
14932 },
14933 lsp::CompletionItem {
14934 label: "single line label 2".to_string(),
14935 label_details: Some(lsp::CompletionItemLabelDetails {
14936 description: Some(multiline_description.to_string()),
14937 detail: None,
14938 }),
14939 text_edit: gen_text_edit(¶ms, "new_text_2"),
14940 ..lsp::CompletionItem::default()
14941 },
14942 lsp::CompletionItem {
14943 label: multiline_label_2.to_string(),
14944 detail: Some(multiline_detail_2.to_string()),
14945 text_edit: gen_text_edit(¶ms, "new_text_3"),
14946 ..lsp::CompletionItem::default()
14947 },
14948 lsp::CompletionItem {
14949 label: "Label with many spaces and \t but without newlines".to_string(),
14950 detail: Some(
14951 "Details with many spaces and \t but without newlines".to_string(),
14952 ),
14953 text_edit: gen_text_edit(¶ms, "new_text_4"),
14954 ..lsp::CompletionItem::default()
14955 },
14956 ])))
14957 },
14958 );
14959
14960 editor.update_in(cx, |editor, window, cx| {
14961 cx.focus_self(window);
14962 editor.move_to_end(&MoveToEnd, window, cx);
14963 editor.handle_input(".", window, cx);
14964 });
14965 cx.run_until_parked();
14966 completion_handle.next().await.unwrap();
14967
14968 editor.update(cx, |editor, _| {
14969 assert!(editor.context_menu_visible());
14970 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14971 {
14972 let completion_labels = menu
14973 .completions
14974 .borrow()
14975 .iter()
14976 .map(|c| c.label.text.clone())
14977 .collect::<Vec<_>>();
14978 assert_eq!(
14979 completion_labels,
14980 &[
14981 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14982 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14983 "single line label 2 d e f ",
14984 "a b c g h i ",
14985 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14986 ],
14987 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14988 );
14989
14990 for completion in menu
14991 .completions
14992 .borrow()
14993 .iter() {
14994 assert_eq!(
14995 completion.label.filter_range,
14996 0..completion.label.text.len(),
14997 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14998 );
14999 }
15000 } else {
15001 panic!("expected completion menu to be open");
15002 }
15003 });
15004}
15005
15006#[gpui::test]
15007async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15008 init_test(cx, |_| {});
15009 let mut cx = EditorLspTestContext::new_rust(
15010 lsp::ServerCapabilities {
15011 completion_provider: Some(lsp::CompletionOptions {
15012 trigger_characters: Some(vec![".".to_string()]),
15013 ..Default::default()
15014 }),
15015 ..Default::default()
15016 },
15017 cx,
15018 )
15019 .await;
15020 cx.lsp
15021 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15022 Ok(Some(lsp::CompletionResponse::Array(vec![
15023 lsp::CompletionItem {
15024 label: "first".into(),
15025 ..Default::default()
15026 },
15027 lsp::CompletionItem {
15028 label: "last".into(),
15029 ..Default::default()
15030 },
15031 ])))
15032 });
15033 cx.set_state("variableˇ");
15034 cx.simulate_keystroke(".");
15035 cx.executor().run_until_parked();
15036
15037 cx.update_editor(|editor, _, _| {
15038 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15039 {
15040 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15041 } else {
15042 panic!("expected completion menu to be open");
15043 }
15044 });
15045
15046 cx.update_editor(|editor, window, cx| {
15047 editor.move_page_down(&MovePageDown::default(), window, cx);
15048 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15049 {
15050 assert!(
15051 menu.selected_item == 1,
15052 "expected PageDown to select the last item from the context menu"
15053 );
15054 } else {
15055 panic!("expected completion menu to stay open after PageDown");
15056 }
15057 });
15058
15059 cx.update_editor(|editor, window, cx| {
15060 editor.move_page_up(&MovePageUp::default(), window, cx);
15061 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15062 {
15063 assert!(
15064 menu.selected_item == 0,
15065 "expected PageUp to select the first item from the context menu"
15066 );
15067 } else {
15068 panic!("expected completion menu to stay open after PageUp");
15069 }
15070 });
15071}
15072
15073#[gpui::test]
15074async fn test_as_is_completions(cx: &mut TestAppContext) {
15075 init_test(cx, |_| {});
15076 let mut cx = EditorLspTestContext::new_rust(
15077 lsp::ServerCapabilities {
15078 completion_provider: Some(lsp::CompletionOptions {
15079 ..Default::default()
15080 }),
15081 ..Default::default()
15082 },
15083 cx,
15084 )
15085 .await;
15086 cx.lsp
15087 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15088 Ok(Some(lsp::CompletionResponse::Array(vec![
15089 lsp::CompletionItem {
15090 label: "unsafe".into(),
15091 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15092 range: lsp::Range {
15093 start: lsp::Position {
15094 line: 1,
15095 character: 2,
15096 },
15097 end: lsp::Position {
15098 line: 1,
15099 character: 3,
15100 },
15101 },
15102 new_text: "unsafe".to_string(),
15103 })),
15104 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15105 ..Default::default()
15106 },
15107 ])))
15108 });
15109 cx.set_state("fn a() {}\n nˇ");
15110 cx.executor().run_until_parked();
15111 cx.update_editor(|editor, window, cx| {
15112 editor.show_completions(
15113 &ShowCompletions {
15114 trigger: Some("\n".into()),
15115 },
15116 window,
15117 cx,
15118 );
15119 });
15120 cx.executor().run_until_parked();
15121
15122 cx.update_editor(|editor, window, cx| {
15123 editor.confirm_completion(&Default::default(), window, cx)
15124 });
15125 cx.executor().run_until_parked();
15126 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15127}
15128
15129#[gpui::test]
15130async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15131 init_test(cx, |_| {});
15132 let language =
15133 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15134 let mut cx = EditorLspTestContext::new(
15135 language,
15136 lsp::ServerCapabilities {
15137 completion_provider: Some(lsp::CompletionOptions {
15138 ..lsp::CompletionOptions::default()
15139 }),
15140 ..lsp::ServerCapabilities::default()
15141 },
15142 cx,
15143 )
15144 .await;
15145
15146 cx.set_state(
15147 "#ifndef BAR_H
15148#define BAR_H
15149
15150#include <stdbool.h>
15151
15152int fn_branch(bool do_branch1, bool do_branch2);
15153
15154#endif // BAR_H
15155ˇ",
15156 );
15157 cx.executor().run_until_parked();
15158 cx.update_editor(|editor, window, cx| {
15159 editor.handle_input("#", window, cx);
15160 });
15161 cx.executor().run_until_parked();
15162 cx.update_editor(|editor, window, cx| {
15163 editor.handle_input("i", window, cx);
15164 });
15165 cx.executor().run_until_parked();
15166 cx.update_editor(|editor, window, cx| {
15167 editor.handle_input("n", window, cx);
15168 });
15169 cx.executor().run_until_parked();
15170 cx.assert_editor_state(
15171 "#ifndef BAR_H
15172#define BAR_H
15173
15174#include <stdbool.h>
15175
15176int fn_branch(bool do_branch1, bool do_branch2);
15177
15178#endif // BAR_H
15179#inˇ",
15180 );
15181
15182 cx.lsp
15183 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15184 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15185 is_incomplete: false,
15186 item_defaults: None,
15187 items: vec![lsp::CompletionItem {
15188 kind: Some(lsp::CompletionItemKind::SNIPPET),
15189 label_details: Some(lsp::CompletionItemLabelDetails {
15190 detail: Some("header".to_string()),
15191 description: None,
15192 }),
15193 label: " include".to_string(),
15194 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15195 range: lsp::Range {
15196 start: lsp::Position {
15197 line: 8,
15198 character: 1,
15199 },
15200 end: lsp::Position {
15201 line: 8,
15202 character: 1,
15203 },
15204 },
15205 new_text: "include \"$0\"".to_string(),
15206 })),
15207 sort_text: Some("40b67681include".to_string()),
15208 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15209 filter_text: Some("include".to_string()),
15210 insert_text: Some("include \"$0\"".to_string()),
15211 ..lsp::CompletionItem::default()
15212 }],
15213 })))
15214 });
15215 cx.update_editor(|editor, window, cx| {
15216 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15217 });
15218 cx.executor().run_until_parked();
15219 cx.update_editor(|editor, window, cx| {
15220 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15221 });
15222 cx.executor().run_until_parked();
15223 cx.assert_editor_state(
15224 "#ifndef BAR_H
15225#define BAR_H
15226
15227#include <stdbool.h>
15228
15229int fn_branch(bool do_branch1, bool do_branch2);
15230
15231#endif // BAR_H
15232#include \"ˇ\"",
15233 );
15234
15235 cx.lsp
15236 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15237 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15238 is_incomplete: true,
15239 item_defaults: None,
15240 items: vec![lsp::CompletionItem {
15241 kind: Some(lsp::CompletionItemKind::FILE),
15242 label: "AGL/".to_string(),
15243 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15244 range: lsp::Range {
15245 start: lsp::Position {
15246 line: 8,
15247 character: 10,
15248 },
15249 end: lsp::Position {
15250 line: 8,
15251 character: 11,
15252 },
15253 },
15254 new_text: "AGL/".to_string(),
15255 })),
15256 sort_text: Some("40b67681AGL/".to_string()),
15257 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15258 filter_text: Some("AGL/".to_string()),
15259 insert_text: Some("AGL/".to_string()),
15260 ..lsp::CompletionItem::default()
15261 }],
15262 })))
15263 });
15264 cx.update_editor(|editor, window, cx| {
15265 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15266 });
15267 cx.executor().run_until_parked();
15268 cx.update_editor(|editor, window, cx| {
15269 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15270 });
15271 cx.executor().run_until_parked();
15272 cx.assert_editor_state(
15273 r##"#ifndef BAR_H
15274#define BAR_H
15275
15276#include <stdbool.h>
15277
15278int fn_branch(bool do_branch1, bool do_branch2);
15279
15280#endif // BAR_H
15281#include "AGL/ˇ"##,
15282 );
15283
15284 cx.update_editor(|editor, window, cx| {
15285 editor.handle_input("\"", window, cx);
15286 });
15287 cx.executor().run_until_parked();
15288 cx.assert_editor_state(
15289 r##"#ifndef BAR_H
15290#define BAR_H
15291
15292#include <stdbool.h>
15293
15294int fn_branch(bool do_branch1, bool do_branch2);
15295
15296#endif // BAR_H
15297#include "AGL/"ˇ"##,
15298 );
15299}
15300
15301#[gpui::test]
15302async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15303 init_test(cx, |_| {});
15304
15305 let mut cx = EditorLspTestContext::new_rust(
15306 lsp::ServerCapabilities {
15307 completion_provider: Some(lsp::CompletionOptions {
15308 trigger_characters: Some(vec![".".to_string()]),
15309 resolve_provider: Some(true),
15310 ..Default::default()
15311 }),
15312 ..Default::default()
15313 },
15314 cx,
15315 )
15316 .await;
15317
15318 cx.set_state("fn main() { let a = 2ˇ; }");
15319 cx.simulate_keystroke(".");
15320 let completion_item = lsp::CompletionItem {
15321 label: "Some".into(),
15322 kind: Some(lsp::CompletionItemKind::SNIPPET),
15323 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15324 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15325 kind: lsp::MarkupKind::Markdown,
15326 value: "```rust\nSome(2)\n```".to_string(),
15327 })),
15328 deprecated: Some(false),
15329 sort_text: Some("Some".to_string()),
15330 filter_text: Some("Some".to_string()),
15331 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15332 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15333 range: lsp::Range {
15334 start: lsp::Position {
15335 line: 0,
15336 character: 22,
15337 },
15338 end: lsp::Position {
15339 line: 0,
15340 character: 22,
15341 },
15342 },
15343 new_text: "Some(2)".to_string(),
15344 })),
15345 additional_text_edits: Some(vec![lsp::TextEdit {
15346 range: lsp::Range {
15347 start: lsp::Position {
15348 line: 0,
15349 character: 20,
15350 },
15351 end: lsp::Position {
15352 line: 0,
15353 character: 22,
15354 },
15355 },
15356 new_text: "".to_string(),
15357 }]),
15358 ..Default::default()
15359 };
15360
15361 let closure_completion_item = completion_item.clone();
15362 let counter = Arc::new(AtomicUsize::new(0));
15363 let counter_clone = counter.clone();
15364 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15365 let task_completion_item = closure_completion_item.clone();
15366 counter_clone.fetch_add(1, atomic::Ordering::Release);
15367 async move {
15368 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15369 is_incomplete: true,
15370 item_defaults: None,
15371 items: vec![task_completion_item],
15372 })))
15373 }
15374 });
15375
15376 cx.condition(|editor, _| editor.context_menu_visible())
15377 .await;
15378 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15379 assert!(request.next().await.is_some());
15380 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15381
15382 cx.simulate_keystrokes("S o m");
15383 cx.condition(|editor, _| editor.context_menu_visible())
15384 .await;
15385 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15386 assert!(request.next().await.is_some());
15387 assert!(request.next().await.is_some());
15388 assert!(request.next().await.is_some());
15389 request.close();
15390 assert!(request.next().await.is_none());
15391 assert_eq!(
15392 counter.load(atomic::Ordering::Acquire),
15393 4,
15394 "With the completions menu open, only one LSP request should happen per input"
15395 );
15396}
15397
15398#[gpui::test]
15399async fn test_toggle_comment(cx: &mut TestAppContext) {
15400 init_test(cx, |_| {});
15401 let mut cx = EditorTestContext::new(cx).await;
15402 let language = Arc::new(Language::new(
15403 LanguageConfig {
15404 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15405 ..Default::default()
15406 },
15407 Some(tree_sitter_rust::LANGUAGE.into()),
15408 ));
15409 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15410
15411 // If multiple selections intersect a line, the line is only toggled once.
15412 cx.set_state(indoc! {"
15413 fn a() {
15414 «//b();
15415 ˇ»// «c();
15416 //ˇ» d();
15417 }
15418 "});
15419
15420 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15421
15422 cx.assert_editor_state(indoc! {"
15423 fn a() {
15424 «b();
15425 c();
15426 ˇ» d();
15427 }
15428 "});
15429
15430 // The comment prefix is inserted at the same column for every line in a
15431 // selection.
15432 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15433
15434 cx.assert_editor_state(indoc! {"
15435 fn a() {
15436 // «b();
15437 // c();
15438 ˇ»// d();
15439 }
15440 "});
15441
15442 // If a selection ends at the beginning of a line, that line is not toggled.
15443 cx.set_selections_state(indoc! {"
15444 fn a() {
15445 // b();
15446 «// c();
15447 ˇ» // d();
15448 }
15449 "});
15450
15451 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15452
15453 cx.assert_editor_state(indoc! {"
15454 fn a() {
15455 // b();
15456 «c();
15457 ˇ» // d();
15458 }
15459 "});
15460
15461 // If a selection span a single line and is empty, the line is toggled.
15462 cx.set_state(indoc! {"
15463 fn a() {
15464 a();
15465 b();
15466 ˇ
15467 }
15468 "});
15469
15470 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15471
15472 cx.assert_editor_state(indoc! {"
15473 fn a() {
15474 a();
15475 b();
15476 //•ˇ
15477 }
15478 "});
15479
15480 // If a selection span multiple lines, empty lines are not toggled.
15481 cx.set_state(indoc! {"
15482 fn a() {
15483 «a();
15484
15485 c();ˇ»
15486 }
15487 "});
15488
15489 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15490
15491 cx.assert_editor_state(indoc! {"
15492 fn a() {
15493 // «a();
15494
15495 // c();ˇ»
15496 }
15497 "});
15498
15499 // If a selection includes multiple comment prefixes, all lines are uncommented.
15500 cx.set_state(indoc! {"
15501 fn a() {
15502 «// a();
15503 /// b();
15504 //! c();ˇ»
15505 }
15506 "});
15507
15508 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15509
15510 cx.assert_editor_state(indoc! {"
15511 fn a() {
15512 «a();
15513 b();
15514 c();ˇ»
15515 }
15516 "});
15517}
15518
15519#[gpui::test]
15520async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15521 init_test(cx, |_| {});
15522 let mut cx = EditorTestContext::new(cx).await;
15523 let language = Arc::new(Language::new(
15524 LanguageConfig {
15525 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15526 ..Default::default()
15527 },
15528 Some(tree_sitter_rust::LANGUAGE.into()),
15529 ));
15530 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15531
15532 let toggle_comments = &ToggleComments {
15533 advance_downwards: false,
15534 ignore_indent: true,
15535 };
15536
15537 // If multiple selections intersect a line, the line is only toggled once.
15538 cx.set_state(indoc! {"
15539 fn a() {
15540 // «b();
15541 // c();
15542 // ˇ» d();
15543 }
15544 "});
15545
15546 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15547
15548 cx.assert_editor_state(indoc! {"
15549 fn a() {
15550 «b();
15551 c();
15552 ˇ» d();
15553 }
15554 "});
15555
15556 // The comment prefix is inserted at the beginning of each line
15557 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15558
15559 cx.assert_editor_state(indoc! {"
15560 fn a() {
15561 // «b();
15562 // c();
15563 // ˇ» d();
15564 }
15565 "});
15566
15567 // If a selection ends at the beginning of a line, that line is not toggled.
15568 cx.set_selections_state(indoc! {"
15569 fn a() {
15570 // b();
15571 // «c();
15572 ˇ»// d();
15573 }
15574 "});
15575
15576 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15577
15578 cx.assert_editor_state(indoc! {"
15579 fn a() {
15580 // b();
15581 «c();
15582 ˇ»// d();
15583 }
15584 "});
15585
15586 // If a selection span a single line and is empty, the line is toggled.
15587 cx.set_state(indoc! {"
15588 fn a() {
15589 a();
15590 b();
15591 ˇ
15592 }
15593 "});
15594
15595 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15596
15597 cx.assert_editor_state(indoc! {"
15598 fn a() {
15599 a();
15600 b();
15601 //ˇ
15602 }
15603 "});
15604
15605 // If a selection span multiple lines, empty lines are not toggled.
15606 cx.set_state(indoc! {"
15607 fn a() {
15608 «a();
15609
15610 c();ˇ»
15611 }
15612 "});
15613
15614 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15615
15616 cx.assert_editor_state(indoc! {"
15617 fn a() {
15618 // «a();
15619
15620 // c();ˇ»
15621 }
15622 "});
15623
15624 // If a selection includes multiple comment prefixes, all lines are uncommented.
15625 cx.set_state(indoc! {"
15626 fn a() {
15627 // «a();
15628 /// b();
15629 //! c();ˇ»
15630 }
15631 "});
15632
15633 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15634
15635 cx.assert_editor_state(indoc! {"
15636 fn a() {
15637 «a();
15638 b();
15639 c();ˇ»
15640 }
15641 "});
15642}
15643
15644#[gpui::test]
15645async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15646 init_test(cx, |_| {});
15647
15648 let language = Arc::new(Language::new(
15649 LanguageConfig {
15650 line_comments: vec!["// ".into()],
15651 ..Default::default()
15652 },
15653 Some(tree_sitter_rust::LANGUAGE.into()),
15654 ));
15655
15656 let mut cx = EditorTestContext::new(cx).await;
15657
15658 cx.language_registry().add(language.clone());
15659 cx.update_buffer(|buffer, cx| {
15660 buffer.set_language(Some(language), cx);
15661 });
15662
15663 let toggle_comments = &ToggleComments {
15664 advance_downwards: true,
15665 ignore_indent: false,
15666 };
15667
15668 // Single cursor on one line -> advance
15669 // Cursor moves horizontally 3 characters as well on non-blank line
15670 cx.set_state(indoc!(
15671 "fn a() {
15672 ˇdog();
15673 cat();
15674 }"
15675 ));
15676 cx.update_editor(|editor, window, cx| {
15677 editor.toggle_comments(toggle_comments, window, cx);
15678 });
15679 cx.assert_editor_state(indoc!(
15680 "fn a() {
15681 // dog();
15682 catˇ();
15683 }"
15684 ));
15685
15686 // Single selection on one line -> don't advance
15687 cx.set_state(indoc!(
15688 "fn a() {
15689 «dog()ˇ»;
15690 cat();
15691 }"
15692 ));
15693 cx.update_editor(|editor, window, cx| {
15694 editor.toggle_comments(toggle_comments, window, cx);
15695 });
15696 cx.assert_editor_state(indoc!(
15697 "fn a() {
15698 // «dog()ˇ»;
15699 cat();
15700 }"
15701 ));
15702
15703 // Multiple cursors on one line -> advance
15704 cx.set_state(indoc!(
15705 "fn a() {
15706 ˇdˇog();
15707 cat();
15708 }"
15709 ));
15710 cx.update_editor(|editor, window, cx| {
15711 editor.toggle_comments(toggle_comments, window, cx);
15712 });
15713 cx.assert_editor_state(indoc!(
15714 "fn a() {
15715 // dog();
15716 catˇ(ˇ);
15717 }"
15718 ));
15719
15720 // Multiple cursors on one line, with selection -> don't advance
15721 cx.set_state(indoc!(
15722 "fn a() {
15723 ˇdˇog«()ˇ»;
15724 cat();
15725 }"
15726 ));
15727 cx.update_editor(|editor, window, cx| {
15728 editor.toggle_comments(toggle_comments, window, cx);
15729 });
15730 cx.assert_editor_state(indoc!(
15731 "fn a() {
15732 // ˇdˇog«()ˇ»;
15733 cat();
15734 }"
15735 ));
15736
15737 // Single cursor on one line -> advance
15738 // Cursor moves to column 0 on blank line
15739 cx.set_state(indoc!(
15740 "fn a() {
15741 ˇdog();
15742
15743 cat();
15744 }"
15745 ));
15746 cx.update_editor(|editor, window, cx| {
15747 editor.toggle_comments(toggle_comments, window, cx);
15748 });
15749 cx.assert_editor_state(indoc!(
15750 "fn a() {
15751 // dog();
15752 ˇ
15753 cat();
15754 }"
15755 ));
15756
15757 // Single cursor on one line -> advance
15758 // Cursor starts and ends at column 0
15759 cx.set_state(indoc!(
15760 "fn a() {
15761 ˇ dog();
15762 cat();
15763 }"
15764 ));
15765 cx.update_editor(|editor, window, cx| {
15766 editor.toggle_comments(toggle_comments, window, cx);
15767 });
15768 cx.assert_editor_state(indoc!(
15769 "fn a() {
15770 // dog();
15771 ˇ cat();
15772 }"
15773 ));
15774}
15775
15776#[gpui::test]
15777async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15778 init_test(cx, |_| {});
15779
15780 let mut cx = EditorTestContext::new(cx).await;
15781
15782 let html_language = Arc::new(
15783 Language::new(
15784 LanguageConfig {
15785 name: "HTML".into(),
15786 block_comment: Some(BlockCommentConfig {
15787 start: "<!-- ".into(),
15788 prefix: "".into(),
15789 end: " -->".into(),
15790 tab_size: 0,
15791 }),
15792 ..Default::default()
15793 },
15794 Some(tree_sitter_html::LANGUAGE.into()),
15795 )
15796 .with_injection_query(
15797 r#"
15798 (script_element
15799 (raw_text) @injection.content
15800 (#set! injection.language "javascript"))
15801 "#,
15802 )
15803 .unwrap(),
15804 );
15805
15806 let javascript_language = Arc::new(Language::new(
15807 LanguageConfig {
15808 name: "JavaScript".into(),
15809 line_comments: vec!["// ".into()],
15810 ..Default::default()
15811 },
15812 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15813 ));
15814
15815 cx.language_registry().add(html_language.clone());
15816 cx.language_registry().add(javascript_language);
15817 cx.update_buffer(|buffer, cx| {
15818 buffer.set_language(Some(html_language), cx);
15819 });
15820
15821 // Toggle comments for empty selections
15822 cx.set_state(
15823 &r#"
15824 <p>A</p>ˇ
15825 <p>B</p>ˇ
15826 <p>C</p>ˇ
15827 "#
15828 .unindent(),
15829 );
15830 cx.update_editor(|editor, window, cx| {
15831 editor.toggle_comments(&ToggleComments::default(), window, cx)
15832 });
15833 cx.assert_editor_state(
15834 &r#"
15835 <!-- <p>A</p>ˇ -->
15836 <!-- <p>B</p>ˇ -->
15837 <!-- <p>C</p>ˇ -->
15838 "#
15839 .unindent(),
15840 );
15841 cx.update_editor(|editor, window, cx| {
15842 editor.toggle_comments(&ToggleComments::default(), window, cx)
15843 });
15844 cx.assert_editor_state(
15845 &r#"
15846 <p>A</p>ˇ
15847 <p>B</p>ˇ
15848 <p>C</p>ˇ
15849 "#
15850 .unindent(),
15851 );
15852
15853 // Toggle comments for mixture of empty and non-empty selections, where
15854 // multiple selections occupy a given line.
15855 cx.set_state(
15856 &r#"
15857 <p>A«</p>
15858 <p>ˇ»B</p>ˇ
15859 <p>C«</p>
15860 <p>ˇ»D</p>ˇ
15861 "#
15862 .unindent(),
15863 );
15864
15865 cx.update_editor(|editor, window, cx| {
15866 editor.toggle_comments(&ToggleComments::default(), window, cx)
15867 });
15868 cx.assert_editor_state(
15869 &r#"
15870 <!-- <p>A«</p>
15871 <p>ˇ»B</p>ˇ -->
15872 <!-- <p>C«</p>
15873 <p>ˇ»D</p>ˇ -->
15874 "#
15875 .unindent(),
15876 );
15877 cx.update_editor(|editor, window, cx| {
15878 editor.toggle_comments(&ToggleComments::default(), window, cx)
15879 });
15880 cx.assert_editor_state(
15881 &r#"
15882 <p>A«</p>
15883 <p>ˇ»B</p>ˇ
15884 <p>C«</p>
15885 <p>ˇ»D</p>ˇ
15886 "#
15887 .unindent(),
15888 );
15889
15890 // Toggle comments when different languages are active for different
15891 // selections.
15892 cx.set_state(
15893 &r#"
15894 ˇ<script>
15895 ˇvar x = new Y();
15896 ˇ</script>
15897 "#
15898 .unindent(),
15899 );
15900 cx.executor().run_until_parked();
15901 cx.update_editor(|editor, window, cx| {
15902 editor.toggle_comments(&ToggleComments::default(), window, cx)
15903 });
15904 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15905 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15906 cx.assert_editor_state(
15907 &r#"
15908 <!-- ˇ<script> -->
15909 // ˇvar x = new Y();
15910 <!-- ˇ</script> -->
15911 "#
15912 .unindent(),
15913 );
15914}
15915
15916#[gpui::test]
15917fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15918 init_test(cx, |_| {});
15919
15920 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15921 let multibuffer = cx.new(|cx| {
15922 let mut multibuffer = MultiBuffer::new(ReadWrite);
15923 multibuffer.push_excerpts(
15924 buffer.clone(),
15925 [
15926 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15927 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15928 ],
15929 cx,
15930 );
15931 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15932 multibuffer
15933 });
15934
15935 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15936 editor.update_in(cx, |editor, window, cx| {
15937 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15938 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15939 s.select_ranges([
15940 Point::new(0, 0)..Point::new(0, 0),
15941 Point::new(1, 0)..Point::new(1, 0),
15942 ])
15943 });
15944
15945 editor.handle_input("X", window, cx);
15946 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15947 assert_eq!(
15948 editor.selections.ranges(cx),
15949 [
15950 Point::new(0, 1)..Point::new(0, 1),
15951 Point::new(1, 1)..Point::new(1, 1),
15952 ]
15953 );
15954
15955 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15956 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15957 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15958 });
15959 editor.backspace(&Default::default(), window, cx);
15960 assert_eq!(editor.text(cx), "Xa\nbbb");
15961 assert_eq!(
15962 editor.selections.ranges(cx),
15963 [Point::new(1, 0)..Point::new(1, 0)]
15964 );
15965
15966 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15967 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15968 });
15969 editor.backspace(&Default::default(), window, cx);
15970 assert_eq!(editor.text(cx), "X\nbb");
15971 assert_eq!(
15972 editor.selections.ranges(cx),
15973 [Point::new(0, 1)..Point::new(0, 1)]
15974 );
15975 });
15976}
15977
15978#[gpui::test]
15979fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15980 init_test(cx, |_| {});
15981
15982 let markers = vec![('[', ']').into(), ('(', ')').into()];
15983 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15984 indoc! {"
15985 [aaaa
15986 (bbbb]
15987 cccc)",
15988 },
15989 markers.clone(),
15990 );
15991 let excerpt_ranges = markers.into_iter().map(|marker| {
15992 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15993 ExcerptRange::new(context)
15994 });
15995 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15996 let multibuffer = cx.new(|cx| {
15997 let mut multibuffer = MultiBuffer::new(ReadWrite);
15998 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15999 multibuffer
16000 });
16001
16002 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16003 editor.update_in(cx, |editor, window, cx| {
16004 let (expected_text, selection_ranges) = marked_text_ranges(
16005 indoc! {"
16006 aaaa
16007 bˇbbb
16008 bˇbbˇb
16009 cccc"
16010 },
16011 true,
16012 );
16013 assert_eq!(editor.text(cx), expected_text);
16014 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16015 s.select_ranges(selection_ranges)
16016 });
16017
16018 editor.handle_input("X", window, cx);
16019
16020 let (expected_text, expected_selections) = marked_text_ranges(
16021 indoc! {"
16022 aaaa
16023 bXˇbbXb
16024 bXˇbbXˇb
16025 cccc"
16026 },
16027 false,
16028 );
16029 assert_eq!(editor.text(cx), expected_text);
16030 assert_eq!(editor.selections.ranges(cx), expected_selections);
16031
16032 editor.newline(&Newline, window, cx);
16033 let (expected_text, expected_selections) = marked_text_ranges(
16034 indoc! {"
16035 aaaa
16036 bX
16037 ˇbbX
16038 b
16039 bX
16040 ˇbbX
16041 ˇb
16042 cccc"
16043 },
16044 false,
16045 );
16046 assert_eq!(editor.text(cx), expected_text);
16047 assert_eq!(editor.selections.ranges(cx), expected_selections);
16048 });
16049}
16050
16051#[gpui::test]
16052fn test_refresh_selections(cx: &mut TestAppContext) {
16053 init_test(cx, |_| {});
16054
16055 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16056 let mut excerpt1_id = None;
16057 let multibuffer = cx.new(|cx| {
16058 let mut multibuffer = MultiBuffer::new(ReadWrite);
16059 excerpt1_id = multibuffer
16060 .push_excerpts(
16061 buffer.clone(),
16062 [
16063 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16064 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16065 ],
16066 cx,
16067 )
16068 .into_iter()
16069 .next();
16070 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16071 multibuffer
16072 });
16073
16074 let editor = cx.add_window(|window, cx| {
16075 let mut editor = build_editor(multibuffer.clone(), window, cx);
16076 let snapshot = editor.snapshot(window, cx);
16077 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16078 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16079 });
16080 editor.begin_selection(
16081 Point::new(2, 1).to_display_point(&snapshot),
16082 true,
16083 1,
16084 window,
16085 cx,
16086 );
16087 assert_eq!(
16088 editor.selections.ranges(cx),
16089 [
16090 Point::new(1, 3)..Point::new(1, 3),
16091 Point::new(2, 1)..Point::new(2, 1),
16092 ]
16093 );
16094 editor
16095 });
16096
16097 // Refreshing selections is a no-op when excerpts haven't changed.
16098 _ = editor.update(cx, |editor, window, cx| {
16099 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16100 assert_eq!(
16101 editor.selections.ranges(cx),
16102 [
16103 Point::new(1, 3)..Point::new(1, 3),
16104 Point::new(2, 1)..Point::new(2, 1),
16105 ]
16106 );
16107 });
16108
16109 multibuffer.update(cx, |multibuffer, cx| {
16110 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16111 });
16112 _ = editor.update(cx, |editor, window, cx| {
16113 // Removing an excerpt causes the first selection to become degenerate.
16114 assert_eq!(
16115 editor.selections.ranges(cx),
16116 [
16117 Point::new(0, 0)..Point::new(0, 0),
16118 Point::new(0, 1)..Point::new(0, 1)
16119 ]
16120 );
16121
16122 // Refreshing selections will relocate the first selection to the original buffer
16123 // location.
16124 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16125 assert_eq!(
16126 editor.selections.ranges(cx),
16127 [
16128 Point::new(0, 1)..Point::new(0, 1),
16129 Point::new(0, 3)..Point::new(0, 3)
16130 ]
16131 );
16132 assert!(editor.selections.pending_anchor().is_some());
16133 });
16134}
16135
16136#[gpui::test]
16137fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16138 init_test(cx, |_| {});
16139
16140 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16141 let mut excerpt1_id = None;
16142 let multibuffer = cx.new(|cx| {
16143 let mut multibuffer = MultiBuffer::new(ReadWrite);
16144 excerpt1_id = multibuffer
16145 .push_excerpts(
16146 buffer.clone(),
16147 [
16148 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16149 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16150 ],
16151 cx,
16152 )
16153 .into_iter()
16154 .next();
16155 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16156 multibuffer
16157 });
16158
16159 let editor = cx.add_window(|window, cx| {
16160 let mut editor = build_editor(multibuffer.clone(), window, cx);
16161 let snapshot = editor.snapshot(window, cx);
16162 editor.begin_selection(
16163 Point::new(1, 3).to_display_point(&snapshot),
16164 false,
16165 1,
16166 window,
16167 cx,
16168 );
16169 assert_eq!(
16170 editor.selections.ranges(cx),
16171 [Point::new(1, 3)..Point::new(1, 3)]
16172 );
16173 editor
16174 });
16175
16176 multibuffer.update(cx, |multibuffer, cx| {
16177 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16178 });
16179 _ = editor.update(cx, |editor, window, cx| {
16180 assert_eq!(
16181 editor.selections.ranges(cx),
16182 [Point::new(0, 0)..Point::new(0, 0)]
16183 );
16184
16185 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16186 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16187 assert_eq!(
16188 editor.selections.ranges(cx),
16189 [Point::new(0, 3)..Point::new(0, 3)]
16190 );
16191 assert!(editor.selections.pending_anchor().is_some());
16192 });
16193}
16194
16195#[gpui::test]
16196async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16197 init_test(cx, |_| {});
16198
16199 let language = Arc::new(
16200 Language::new(
16201 LanguageConfig {
16202 brackets: BracketPairConfig {
16203 pairs: vec![
16204 BracketPair {
16205 start: "{".to_string(),
16206 end: "}".to_string(),
16207 close: true,
16208 surround: true,
16209 newline: true,
16210 },
16211 BracketPair {
16212 start: "/* ".to_string(),
16213 end: " */".to_string(),
16214 close: true,
16215 surround: true,
16216 newline: true,
16217 },
16218 ],
16219 ..Default::default()
16220 },
16221 ..Default::default()
16222 },
16223 Some(tree_sitter_rust::LANGUAGE.into()),
16224 )
16225 .with_indents_query("")
16226 .unwrap(),
16227 );
16228
16229 let text = concat!(
16230 "{ }\n", //
16231 " x\n", //
16232 " /* */\n", //
16233 "x\n", //
16234 "{{} }\n", //
16235 );
16236
16237 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16238 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16239 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16240 editor
16241 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16242 .await;
16243
16244 editor.update_in(cx, |editor, window, cx| {
16245 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16246 s.select_display_ranges([
16247 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16248 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16249 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16250 ])
16251 });
16252 editor.newline(&Newline, window, cx);
16253
16254 assert_eq!(
16255 editor.buffer().read(cx).read(cx).text(),
16256 concat!(
16257 "{ \n", // Suppress rustfmt
16258 "\n", //
16259 "}\n", //
16260 " x\n", //
16261 " /* \n", //
16262 " \n", //
16263 " */\n", //
16264 "x\n", //
16265 "{{} \n", //
16266 "}\n", //
16267 )
16268 );
16269 });
16270}
16271
16272#[gpui::test]
16273fn test_highlighted_ranges(cx: &mut TestAppContext) {
16274 init_test(cx, |_| {});
16275
16276 let editor = cx.add_window(|window, cx| {
16277 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16278 build_editor(buffer, window, cx)
16279 });
16280
16281 _ = editor.update(cx, |editor, window, cx| {
16282 struct Type1;
16283 struct Type2;
16284
16285 let buffer = editor.buffer.read(cx).snapshot(cx);
16286
16287 let anchor_range =
16288 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16289
16290 editor.highlight_background::<Type1>(
16291 &[
16292 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16293 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16294 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16295 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16296 ],
16297 |_| Hsla::red(),
16298 cx,
16299 );
16300 editor.highlight_background::<Type2>(
16301 &[
16302 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16303 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16304 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16305 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16306 ],
16307 |_| Hsla::green(),
16308 cx,
16309 );
16310
16311 let snapshot = editor.snapshot(window, cx);
16312 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16313 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16314 &snapshot,
16315 cx.theme(),
16316 );
16317 assert_eq!(
16318 highlighted_ranges,
16319 &[
16320 (
16321 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16322 Hsla::green(),
16323 ),
16324 (
16325 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16326 Hsla::red(),
16327 ),
16328 (
16329 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16330 Hsla::green(),
16331 ),
16332 (
16333 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16334 Hsla::red(),
16335 ),
16336 ]
16337 );
16338 assert_eq!(
16339 editor.sorted_background_highlights_in_range(
16340 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16341 &snapshot,
16342 cx.theme(),
16343 ),
16344 &[(
16345 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16346 Hsla::red(),
16347 )]
16348 );
16349 });
16350}
16351
16352#[gpui::test]
16353async fn test_following(cx: &mut TestAppContext) {
16354 init_test(cx, |_| {});
16355
16356 let fs = FakeFs::new(cx.executor());
16357 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16358
16359 let buffer = project.update(cx, |project, cx| {
16360 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16361 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16362 });
16363 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16364 let follower = cx.update(|cx| {
16365 cx.open_window(
16366 WindowOptions {
16367 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16368 gpui::Point::new(px(0.), px(0.)),
16369 gpui::Point::new(px(10.), px(80.)),
16370 ))),
16371 ..Default::default()
16372 },
16373 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16374 )
16375 .unwrap()
16376 });
16377
16378 let is_still_following = Rc::new(RefCell::new(true));
16379 let follower_edit_event_count = Rc::new(RefCell::new(0));
16380 let pending_update = Rc::new(RefCell::new(None));
16381 let leader_entity = leader.root(cx).unwrap();
16382 let follower_entity = follower.root(cx).unwrap();
16383 _ = follower.update(cx, {
16384 let update = pending_update.clone();
16385 let is_still_following = is_still_following.clone();
16386 let follower_edit_event_count = follower_edit_event_count.clone();
16387 |_, window, cx| {
16388 cx.subscribe_in(
16389 &leader_entity,
16390 window,
16391 move |_, leader, event, window, cx| {
16392 leader.read(cx).add_event_to_update_proto(
16393 event,
16394 &mut update.borrow_mut(),
16395 window,
16396 cx,
16397 );
16398 },
16399 )
16400 .detach();
16401
16402 cx.subscribe_in(
16403 &follower_entity,
16404 window,
16405 move |_, _, event: &EditorEvent, _window, _cx| {
16406 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16407 *is_still_following.borrow_mut() = false;
16408 }
16409
16410 if let EditorEvent::BufferEdited = event {
16411 *follower_edit_event_count.borrow_mut() += 1;
16412 }
16413 },
16414 )
16415 .detach();
16416 }
16417 });
16418
16419 // Update the selections only
16420 _ = leader.update(cx, |leader, window, cx| {
16421 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16422 s.select_ranges([1..1])
16423 });
16424 });
16425 follower
16426 .update(cx, |follower, window, cx| {
16427 follower.apply_update_proto(
16428 &project,
16429 pending_update.borrow_mut().take().unwrap(),
16430 window,
16431 cx,
16432 )
16433 })
16434 .unwrap()
16435 .await
16436 .unwrap();
16437 _ = follower.update(cx, |follower, _, cx| {
16438 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
16439 });
16440 assert!(*is_still_following.borrow());
16441 assert_eq!(*follower_edit_event_count.borrow(), 0);
16442
16443 // Update the scroll position only
16444 _ = leader.update(cx, |leader, window, cx| {
16445 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16446 });
16447 follower
16448 .update(cx, |follower, window, cx| {
16449 follower.apply_update_proto(
16450 &project,
16451 pending_update.borrow_mut().take().unwrap(),
16452 window,
16453 cx,
16454 )
16455 })
16456 .unwrap()
16457 .await
16458 .unwrap();
16459 assert_eq!(
16460 follower
16461 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16462 .unwrap(),
16463 gpui::Point::new(1.5, 3.5)
16464 );
16465 assert!(*is_still_following.borrow());
16466 assert_eq!(*follower_edit_event_count.borrow(), 0);
16467
16468 // Update the selections and scroll position. The follower's scroll position is updated
16469 // via autoscroll, not via the leader's exact scroll position.
16470 _ = leader.update(cx, |leader, window, cx| {
16471 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16472 s.select_ranges([0..0])
16473 });
16474 leader.request_autoscroll(Autoscroll::newest(), cx);
16475 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16476 });
16477 follower
16478 .update(cx, |follower, window, cx| {
16479 follower.apply_update_proto(
16480 &project,
16481 pending_update.borrow_mut().take().unwrap(),
16482 window,
16483 cx,
16484 )
16485 })
16486 .unwrap()
16487 .await
16488 .unwrap();
16489 _ = follower.update(cx, |follower, _, cx| {
16490 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16491 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16492 });
16493 assert!(*is_still_following.borrow());
16494
16495 // Creating a pending selection that precedes another selection
16496 _ = leader.update(cx, |leader, window, cx| {
16497 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16498 s.select_ranges([1..1])
16499 });
16500 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16501 });
16502 follower
16503 .update(cx, |follower, window, cx| {
16504 follower.apply_update_proto(
16505 &project,
16506 pending_update.borrow_mut().take().unwrap(),
16507 window,
16508 cx,
16509 )
16510 })
16511 .unwrap()
16512 .await
16513 .unwrap();
16514 _ = follower.update(cx, |follower, _, cx| {
16515 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16516 });
16517 assert!(*is_still_following.borrow());
16518
16519 // Extend the pending selection so that it surrounds another selection
16520 _ = leader.update(cx, |leader, window, cx| {
16521 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16522 });
16523 follower
16524 .update(cx, |follower, window, cx| {
16525 follower.apply_update_proto(
16526 &project,
16527 pending_update.borrow_mut().take().unwrap(),
16528 window,
16529 cx,
16530 )
16531 })
16532 .unwrap()
16533 .await
16534 .unwrap();
16535 _ = follower.update(cx, |follower, _, cx| {
16536 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16537 });
16538
16539 // Scrolling locally breaks the follow
16540 _ = follower.update(cx, |follower, window, cx| {
16541 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16542 follower.set_scroll_anchor(
16543 ScrollAnchor {
16544 anchor: top_anchor,
16545 offset: gpui::Point::new(0.0, 0.5),
16546 },
16547 window,
16548 cx,
16549 );
16550 });
16551 assert!(!(*is_still_following.borrow()));
16552}
16553
16554#[gpui::test]
16555async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16556 init_test(cx, |_| {});
16557
16558 let fs = FakeFs::new(cx.executor());
16559 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16560 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16561 let pane = workspace
16562 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16563 .unwrap();
16564
16565 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16566
16567 let leader = pane.update_in(cx, |_, window, cx| {
16568 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16569 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16570 });
16571
16572 // Start following the editor when it has no excerpts.
16573 let mut state_message =
16574 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16575 let workspace_entity = workspace.root(cx).unwrap();
16576 let follower_1 = cx
16577 .update_window(*workspace.deref(), |_, window, cx| {
16578 Editor::from_state_proto(
16579 workspace_entity,
16580 ViewId {
16581 creator: CollaboratorId::PeerId(PeerId::default()),
16582 id: 0,
16583 },
16584 &mut state_message,
16585 window,
16586 cx,
16587 )
16588 })
16589 .unwrap()
16590 .unwrap()
16591 .await
16592 .unwrap();
16593
16594 let update_message = Rc::new(RefCell::new(None));
16595 follower_1.update_in(cx, {
16596 let update = update_message.clone();
16597 |_, window, cx| {
16598 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16599 leader.read(cx).add_event_to_update_proto(
16600 event,
16601 &mut update.borrow_mut(),
16602 window,
16603 cx,
16604 );
16605 })
16606 .detach();
16607 }
16608 });
16609
16610 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16611 (
16612 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16613 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16614 )
16615 });
16616
16617 // Insert some excerpts.
16618 leader.update(cx, |leader, cx| {
16619 leader.buffer.update(cx, |multibuffer, cx| {
16620 multibuffer.set_excerpts_for_path(
16621 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
16622 buffer_1.clone(),
16623 vec![
16624 Point::row_range(0..3),
16625 Point::row_range(1..6),
16626 Point::row_range(12..15),
16627 ],
16628 0,
16629 cx,
16630 );
16631 multibuffer.set_excerpts_for_path(
16632 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
16633 buffer_2.clone(),
16634 vec![Point::row_range(0..6), Point::row_range(8..12)],
16635 0,
16636 cx,
16637 );
16638 });
16639 });
16640
16641 // Apply the update of adding the excerpts.
16642 follower_1
16643 .update_in(cx, |follower, window, cx| {
16644 follower.apply_update_proto(
16645 &project,
16646 update_message.borrow().clone().unwrap(),
16647 window,
16648 cx,
16649 )
16650 })
16651 .await
16652 .unwrap();
16653 assert_eq!(
16654 follower_1.update(cx, |editor, cx| editor.text(cx)),
16655 leader.update(cx, |editor, cx| editor.text(cx))
16656 );
16657 update_message.borrow_mut().take();
16658
16659 // Start following separately after it already has excerpts.
16660 let mut state_message =
16661 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16662 let workspace_entity = workspace.root(cx).unwrap();
16663 let follower_2 = cx
16664 .update_window(*workspace.deref(), |_, window, cx| {
16665 Editor::from_state_proto(
16666 workspace_entity,
16667 ViewId {
16668 creator: CollaboratorId::PeerId(PeerId::default()),
16669 id: 0,
16670 },
16671 &mut state_message,
16672 window,
16673 cx,
16674 )
16675 })
16676 .unwrap()
16677 .unwrap()
16678 .await
16679 .unwrap();
16680 assert_eq!(
16681 follower_2.update(cx, |editor, cx| editor.text(cx)),
16682 leader.update(cx, |editor, cx| editor.text(cx))
16683 );
16684
16685 // Remove some excerpts.
16686 leader.update(cx, |leader, cx| {
16687 leader.buffer.update(cx, |multibuffer, cx| {
16688 let excerpt_ids = multibuffer.excerpt_ids();
16689 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16690 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16691 });
16692 });
16693
16694 // Apply the update of removing the excerpts.
16695 follower_1
16696 .update_in(cx, |follower, window, cx| {
16697 follower.apply_update_proto(
16698 &project,
16699 update_message.borrow().clone().unwrap(),
16700 window,
16701 cx,
16702 )
16703 })
16704 .await
16705 .unwrap();
16706 follower_2
16707 .update_in(cx, |follower, window, cx| {
16708 follower.apply_update_proto(
16709 &project,
16710 update_message.borrow().clone().unwrap(),
16711 window,
16712 cx,
16713 )
16714 })
16715 .await
16716 .unwrap();
16717 update_message.borrow_mut().take();
16718 assert_eq!(
16719 follower_1.update(cx, |editor, cx| editor.text(cx)),
16720 leader.update(cx, |editor, cx| editor.text(cx))
16721 );
16722}
16723
16724#[gpui::test]
16725async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16726 init_test(cx, |_| {});
16727
16728 let mut cx = EditorTestContext::new(cx).await;
16729 let lsp_store =
16730 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16731
16732 cx.set_state(indoc! {"
16733 ˇfn func(abc def: i32) -> u32 {
16734 }
16735 "});
16736
16737 cx.update(|_, cx| {
16738 lsp_store.update(cx, |lsp_store, cx| {
16739 lsp_store
16740 .update_diagnostics(
16741 LanguageServerId(0),
16742 lsp::PublishDiagnosticsParams {
16743 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16744 version: None,
16745 diagnostics: vec![
16746 lsp::Diagnostic {
16747 range: lsp::Range::new(
16748 lsp::Position::new(0, 11),
16749 lsp::Position::new(0, 12),
16750 ),
16751 severity: Some(lsp::DiagnosticSeverity::ERROR),
16752 ..Default::default()
16753 },
16754 lsp::Diagnostic {
16755 range: lsp::Range::new(
16756 lsp::Position::new(0, 12),
16757 lsp::Position::new(0, 15),
16758 ),
16759 severity: Some(lsp::DiagnosticSeverity::ERROR),
16760 ..Default::default()
16761 },
16762 lsp::Diagnostic {
16763 range: lsp::Range::new(
16764 lsp::Position::new(0, 25),
16765 lsp::Position::new(0, 28),
16766 ),
16767 severity: Some(lsp::DiagnosticSeverity::ERROR),
16768 ..Default::default()
16769 },
16770 ],
16771 },
16772 None,
16773 DiagnosticSourceKind::Pushed,
16774 &[],
16775 cx,
16776 )
16777 .unwrap()
16778 });
16779 });
16780
16781 executor.run_until_parked();
16782
16783 cx.update_editor(|editor, window, cx| {
16784 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16785 });
16786
16787 cx.assert_editor_state(indoc! {"
16788 fn func(abc def: i32) -> ˇu32 {
16789 }
16790 "});
16791
16792 cx.update_editor(|editor, window, cx| {
16793 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16794 });
16795
16796 cx.assert_editor_state(indoc! {"
16797 fn func(abc ˇdef: i32) -> u32 {
16798 }
16799 "});
16800
16801 cx.update_editor(|editor, window, cx| {
16802 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16803 });
16804
16805 cx.assert_editor_state(indoc! {"
16806 fn func(abcˇ def: i32) -> u32 {
16807 }
16808 "});
16809
16810 cx.update_editor(|editor, window, cx| {
16811 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16812 });
16813
16814 cx.assert_editor_state(indoc! {"
16815 fn func(abc def: i32) -> ˇu32 {
16816 }
16817 "});
16818}
16819
16820#[gpui::test]
16821async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16822 init_test(cx, |_| {});
16823
16824 let mut cx = EditorTestContext::new(cx).await;
16825
16826 let diff_base = r#"
16827 use some::mod;
16828
16829 const A: u32 = 42;
16830
16831 fn main() {
16832 println!("hello");
16833
16834 println!("world");
16835 }
16836 "#
16837 .unindent();
16838
16839 // Edits are modified, removed, modified, added
16840 cx.set_state(
16841 &r#"
16842 use some::modified;
16843
16844 ˇ
16845 fn main() {
16846 println!("hello there");
16847
16848 println!("around the");
16849 println!("world");
16850 }
16851 "#
16852 .unindent(),
16853 );
16854
16855 cx.set_head_text(&diff_base);
16856 executor.run_until_parked();
16857
16858 cx.update_editor(|editor, window, cx| {
16859 //Wrap around the bottom of the buffer
16860 for _ in 0..3 {
16861 editor.go_to_next_hunk(&GoToHunk, window, cx);
16862 }
16863 });
16864
16865 cx.assert_editor_state(
16866 &r#"
16867 ˇuse some::modified;
16868
16869
16870 fn main() {
16871 println!("hello there");
16872
16873 println!("around the");
16874 println!("world");
16875 }
16876 "#
16877 .unindent(),
16878 );
16879
16880 cx.update_editor(|editor, window, cx| {
16881 //Wrap around the top of the buffer
16882 for _ in 0..2 {
16883 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16884 }
16885 });
16886
16887 cx.assert_editor_state(
16888 &r#"
16889 use some::modified;
16890
16891
16892 fn main() {
16893 ˇ println!("hello there");
16894
16895 println!("around the");
16896 println!("world");
16897 }
16898 "#
16899 .unindent(),
16900 );
16901
16902 cx.update_editor(|editor, window, cx| {
16903 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16904 });
16905
16906 cx.assert_editor_state(
16907 &r#"
16908 use some::modified;
16909
16910 ˇ
16911 fn main() {
16912 println!("hello there");
16913
16914 println!("around the");
16915 println!("world");
16916 }
16917 "#
16918 .unindent(),
16919 );
16920
16921 cx.update_editor(|editor, window, cx| {
16922 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16923 });
16924
16925 cx.assert_editor_state(
16926 &r#"
16927 ˇuse some::modified;
16928
16929
16930 fn main() {
16931 println!("hello there");
16932
16933 println!("around the");
16934 println!("world");
16935 }
16936 "#
16937 .unindent(),
16938 );
16939
16940 cx.update_editor(|editor, window, cx| {
16941 for _ in 0..2 {
16942 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16943 }
16944 });
16945
16946 cx.assert_editor_state(
16947 &r#"
16948 use some::modified;
16949
16950
16951 fn main() {
16952 ˇ println!("hello there");
16953
16954 println!("around the");
16955 println!("world");
16956 }
16957 "#
16958 .unindent(),
16959 );
16960
16961 cx.update_editor(|editor, window, cx| {
16962 editor.fold(&Fold, window, cx);
16963 });
16964
16965 cx.update_editor(|editor, window, cx| {
16966 editor.go_to_next_hunk(&GoToHunk, window, cx);
16967 });
16968
16969 cx.assert_editor_state(
16970 &r#"
16971 ˇuse some::modified;
16972
16973
16974 fn main() {
16975 println!("hello there");
16976
16977 println!("around the");
16978 println!("world");
16979 }
16980 "#
16981 .unindent(),
16982 );
16983}
16984
16985#[test]
16986fn test_split_words() {
16987 fn split(text: &str) -> Vec<&str> {
16988 split_words(text).collect()
16989 }
16990
16991 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16992 assert_eq!(split("hello_world"), &["hello_", "world"]);
16993 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16994 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16995 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16996 assert_eq!(split("helloworld"), &["helloworld"]);
16997
16998 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16999}
17000
17001#[gpui::test]
17002async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17003 init_test(cx, |_| {});
17004
17005 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17006 let mut assert = |before, after| {
17007 let _state_context = cx.set_state(before);
17008 cx.run_until_parked();
17009 cx.update_editor(|editor, window, cx| {
17010 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17011 });
17012 cx.run_until_parked();
17013 cx.assert_editor_state(after);
17014 };
17015
17016 // Outside bracket jumps to outside of matching bracket
17017 assert("console.logˇ(var);", "console.log(var)ˇ;");
17018 assert("console.log(var)ˇ;", "console.logˇ(var);");
17019
17020 // Inside bracket jumps to inside of matching bracket
17021 assert("console.log(ˇvar);", "console.log(varˇ);");
17022 assert("console.log(varˇ);", "console.log(ˇvar);");
17023
17024 // When outside a bracket and inside, favor jumping to the inside bracket
17025 assert(
17026 "console.log('foo', [1, 2, 3]ˇ);",
17027 "console.log(ˇ'foo', [1, 2, 3]);",
17028 );
17029 assert(
17030 "console.log(ˇ'foo', [1, 2, 3]);",
17031 "console.log('foo', [1, 2, 3]ˇ);",
17032 );
17033
17034 // Bias forward if two options are equally likely
17035 assert(
17036 "let result = curried_fun()ˇ();",
17037 "let result = curried_fun()()ˇ;",
17038 );
17039
17040 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17041 assert(
17042 indoc! {"
17043 function test() {
17044 console.log('test')ˇ
17045 }"},
17046 indoc! {"
17047 function test() {
17048 console.logˇ('test')
17049 }"},
17050 );
17051}
17052
17053#[gpui::test]
17054async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17055 init_test(cx, |_| {});
17056
17057 let fs = FakeFs::new(cx.executor());
17058 fs.insert_tree(
17059 path!("/a"),
17060 json!({
17061 "main.rs": "fn main() { let a = 5; }",
17062 "other.rs": "// Test file",
17063 }),
17064 )
17065 .await;
17066 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17067
17068 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17069 language_registry.add(Arc::new(Language::new(
17070 LanguageConfig {
17071 name: "Rust".into(),
17072 matcher: LanguageMatcher {
17073 path_suffixes: vec!["rs".to_string()],
17074 ..Default::default()
17075 },
17076 brackets: BracketPairConfig {
17077 pairs: vec![BracketPair {
17078 start: "{".to_string(),
17079 end: "}".to_string(),
17080 close: true,
17081 surround: true,
17082 newline: true,
17083 }],
17084 disabled_scopes_by_bracket_ix: Vec::new(),
17085 },
17086 ..Default::default()
17087 },
17088 Some(tree_sitter_rust::LANGUAGE.into()),
17089 )));
17090 let mut fake_servers = language_registry.register_fake_lsp(
17091 "Rust",
17092 FakeLspAdapter {
17093 capabilities: lsp::ServerCapabilities {
17094 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17095 first_trigger_character: "{".to_string(),
17096 more_trigger_character: None,
17097 }),
17098 ..Default::default()
17099 },
17100 ..Default::default()
17101 },
17102 );
17103
17104 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17105
17106 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17107
17108 let worktree_id = workspace
17109 .update(cx, |workspace, _, cx| {
17110 workspace.project().update(cx, |project, cx| {
17111 project.worktrees(cx).next().unwrap().read(cx).id()
17112 })
17113 })
17114 .unwrap();
17115
17116 let buffer = project
17117 .update(cx, |project, cx| {
17118 project.open_local_buffer(path!("/a/main.rs"), cx)
17119 })
17120 .await
17121 .unwrap();
17122 let editor_handle = workspace
17123 .update(cx, |workspace, window, cx| {
17124 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17125 })
17126 .unwrap()
17127 .await
17128 .unwrap()
17129 .downcast::<Editor>()
17130 .unwrap();
17131
17132 cx.executor().start_waiting();
17133 let fake_server = fake_servers.next().await.unwrap();
17134
17135 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17136 |params, _| async move {
17137 assert_eq!(
17138 params.text_document_position.text_document.uri,
17139 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17140 );
17141 assert_eq!(
17142 params.text_document_position.position,
17143 lsp::Position::new(0, 21),
17144 );
17145
17146 Ok(Some(vec![lsp::TextEdit {
17147 new_text: "]".to_string(),
17148 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17149 }]))
17150 },
17151 );
17152
17153 editor_handle.update_in(cx, |editor, window, cx| {
17154 window.focus(&editor.focus_handle(cx));
17155 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17156 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17157 });
17158 editor.handle_input("{", window, cx);
17159 });
17160
17161 cx.executor().run_until_parked();
17162
17163 buffer.update(cx, |buffer, _| {
17164 assert_eq!(
17165 buffer.text(),
17166 "fn main() { let a = {5}; }",
17167 "No extra braces from on type formatting should appear in the buffer"
17168 )
17169 });
17170}
17171
17172#[gpui::test(iterations = 20, seeds(31))]
17173async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17174 init_test(cx, |_| {});
17175
17176 let mut cx = EditorLspTestContext::new_rust(
17177 lsp::ServerCapabilities {
17178 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17179 first_trigger_character: ".".to_string(),
17180 more_trigger_character: None,
17181 }),
17182 ..Default::default()
17183 },
17184 cx,
17185 )
17186 .await;
17187
17188 cx.update_buffer(|buffer, _| {
17189 // This causes autoindent to be async.
17190 buffer.set_sync_parse_timeout(Duration::ZERO)
17191 });
17192
17193 cx.set_state("fn c() {\n d()ˇ\n}\n");
17194 cx.simulate_keystroke("\n");
17195 cx.run_until_parked();
17196
17197 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17198 let mut request =
17199 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17200 let buffer_cloned = buffer_cloned.clone();
17201 async move {
17202 buffer_cloned.update(&mut cx, |buffer, _| {
17203 assert_eq!(
17204 buffer.text(),
17205 "fn c() {\n d()\n .\n}\n",
17206 "OnTypeFormatting should triggered after autoindent applied"
17207 )
17208 })?;
17209
17210 Ok(Some(vec![]))
17211 }
17212 });
17213
17214 cx.simulate_keystroke(".");
17215 cx.run_until_parked();
17216
17217 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17218 assert!(request.next().await.is_some());
17219 request.close();
17220 assert!(request.next().await.is_none());
17221}
17222
17223#[gpui::test]
17224async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17225 init_test(cx, |_| {});
17226
17227 let fs = FakeFs::new(cx.executor());
17228 fs.insert_tree(
17229 path!("/a"),
17230 json!({
17231 "main.rs": "fn main() { let a = 5; }",
17232 "other.rs": "// Test file",
17233 }),
17234 )
17235 .await;
17236
17237 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17238
17239 let server_restarts = Arc::new(AtomicUsize::new(0));
17240 let closure_restarts = Arc::clone(&server_restarts);
17241 let language_server_name = "test language server";
17242 let language_name: LanguageName = "Rust".into();
17243
17244 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17245 language_registry.add(Arc::new(Language::new(
17246 LanguageConfig {
17247 name: language_name.clone(),
17248 matcher: LanguageMatcher {
17249 path_suffixes: vec!["rs".to_string()],
17250 ..Default::default()
17251 },
17252 ..Default::default()
17253 },
17254 Some(tree_sitter_rust::LANGUAGE.into()),
17255 )));
17256 let mut fake_servers = language_registry.register_fake_lsp(
17257 "Rust",
17258 FakeLspAdapter {
17259 name: language_server_name,
17260 initialization_options: Some(json!({
17261 "testOptionValue": true
17262 })),
17263 initializer: Some(Box::new(move |fake_server| {
17264 let task_restarts = Arc::clone(&closure_restarts);
17265 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17266 task_restarts.fetch_add(1, atomic::Ordering::Release);
17267 futures::future::ready(Ok(()))
17268 });
17269 })),
17270 ..Default::default()
17271 },
17272 );
17273
17274 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17275 let _buffer = project
17276 .update(cx, |project, cx| {
17277 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17278 })
17279 .await
17280 .unwrap();
17281 let _fake_server = fake_servers.next().await.unwrap();
17282 update_test_language_settings(cx, |language_settings| {
17283 language_settings.languages.0.insert(
17284 language_name.clone().0,
17285 LanguageSettingsContent {
17286 tab_size: NonZeroU32::new(8),
17287 ..Default::default()
17288 },
17289 );
17290 });
17291 cx.executor().run_until_parked();
17292 assert_eq!(
17293 server_restarts.load(atomic::Ordering::Acquire),
17294 0,
17295 "Should not restart LSP server on an unrelated change"
17296 );
17297
17298 update_test_project_settings(cx, |project_settings| {
17299 project_settings.lsp.insert(
17300 "Some other server name".into(),
17301 LspSettings {
17302 binary: None,
17303 settings: None,
17304 initialization_options: Some(json!({
17305 "some other init value": false
17306 })),
17307 enable_lsp_tasks: false,
17308 fetch: None,
17309 },
17310 );
17311 });
17312 cx.executor().run_until_parked();
17313 assert_eq!(
17314 server_restarts.load(atomic::Ordering::Acquire),
17315 0,
17316 "Should not restart LSP server on an unrelated LSP settings change"
17317 );
17318
17319 update_test_project_settings(cx, |project_settings| {
17320 project_settings.lsp.insert(
17321 language_server_name.into(),
17322 LspSettings {
17323 binary: None,
17324 settings: None,
17325 initialization_options: Some(json!({
17326 "anotherInitValue": false
17327 })),
17328 enable_lsp_tasks: false,
17329 fetch: None,
17330 },
17331 );
17332 });
17333 cx.executor().run_until_parked();
17334 assert_eq!(
17335 server_restarts.load(atomic::Ordering::Acquire),
17336 1,
17337 "Should restart LSP server on a related LSP settings change"
17338 );
17339
17340 update_test_project_settings(cx, |project_settings| {
17341 project_settings.lsp.insert(
17342 language_server_name.into(),
17343 LspSettings {
17344 binary: None,
17345 settings: None,
17346 initialization_options: Some(json!({
17347 "anotherInitValue": false
17348 })),
17349 enable_lsp_tasks: false,
17350 fetch: None,
17351 },
17352 );
17353 });
17354 cx.executor().run_until_parked();
17355 assert_eq!(
17356 server_restarts.load(atomic::Ordering::Acquire),
17357 1,
17358 "Should not restart LSP server on a related LSP settings change that is the same"
17359 );
17360
17361 update_test_project_settings(cx, |project_settings| {
17362 project_settings.lsp.insert(
17363 language_server_name.into(),
17364 LspSettings {
17365 binary: None,
17366 settings: None,
17367 initialization_options: None,
17368 enable_lsp_tasks: false,
17369 fetch: None,
17370 },
17371 );
17372 });
17373 cx.executor().run_until_parked();
17374 assert_eq!(
17375 server_restarts.load(atomic::Ordering::Acquire),
17376 2,
17377 "Should restart LSP server on another related LSP settings change"
17378 );
17379}
17380
17381#[gpui::test]
17382async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17383 init_test(cx, |_| {});
17384
17385 let mut cx = EditorLspTestContext::new_rust(
17386 lsp::ServerCapabilities {
17387 completion_provider: Some(lsp::CompletionOptions {
17388 trigger_characters: Some(vec![".".to_string()]),
17389 resolve_provider: Some(true),
17390 ..Default::default()
17391 }),
17392 ..Default::default()
17393 },
17394 cx,
17395 )
17396 .await;
17397
17398 cx.set_state("fn main() { let a = 2ˇ; }");
17399 cx.simulate_keystroke(".");
17400 let completion_item = lsp::CompletionItem {
17401 label: "some".into(),
17402 kind: Some(lsp::CompletionItemKind::SNIPPET),
17403 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17404 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17405 kind: lsp::MarkupKind::Markdown,
17406 value: "```rust\nSome(2)\n```".to_string(),
17407 })),
17408 deprecated: Some(false),
17409 sort_text: Some("fffffff2".to_string()),
17410 filter_text: Some("some".to_string()),
17411 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17412 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17413 range: lsp::Range {
17414 start: lsp::Position {
17415 line: 0,
17416 character: 22,
17417 },
17418 end: lsp::Position {
17419 line: 0,
17420 character: 22,
17421 },
17422 },
17423 new_text: "Some(2)".to_string(),
17424 })),
17425 additional_text_edits: Some(vec![lsp::TextEdit {
17426 range: lsp::Range {
17427 start: lsp::Position {
17428 line: 0,
17429 character: 20,
17430 },
17431 end: lsp::Position {
17432 line: 0,
17433 character: 22,
17434 },
17435 },
17436 new_text: "".to_string(),
17437 }]),
17438 ..Default::default()
17439 };
17440
17441 let closure_completion_item = completion_item.clone();
17442 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17443 let task_completion_item = closure_completion_item.clone();
17444 async move {
17445 Ok(Some(lsp::CompletionResponse::Array(vec![
17446 task_completion_item,
17447 ])))
17448 }
17449 });
17450
17451 request.next().await;
17452
17453 cx.condition(|editor, _| editor.context_menu_visible())
17454 .await;
17455 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17456 editor
17457 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17458 .unwrap()
17459 });
17460 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17461
17462 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17463 let task_completion_item = completion_item.clone();
17464 async move { Ok(task_completion_item) }
17465 })
17466 .next()
17467 .await
17468 .unwrap();
17469 apply_additional_edits.await.unwrap();
17470 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17471}
17472
17473#[gpui::test]
17474async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17475 init_test(cx, |_| {});
17476
17477 let mut cx = EditorLspTestContext::new_rust(
17478 lsp::ServerCapabilities {
17479 completion_provider: Some(lsp::CompletionOptions {
17480 trigger_characters: Some(vec![".".to_string()]),
17481 resolve_provider: Some(true),
17482 ..Default::default()
17483 }),
17484 ..Default::default()
17485 },
17486 cx,
17487 )
17488 .await;
17489
17490 cx.set_state("fn main() { let a = 2ˇ; }");
17491 cx.simulate_keystroke(".");
17492
17493 let item1 = lsp::CompletionItem {
17494 label: "method id()".to_string(),
17495 filter_text: Some("id".to_string()),
17496 detail: None,
17497 documentation: None,
17498 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17499 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17500 new_text: ".id".to_string(),
17501 })),
17502 ..lsp::CompletionItem::default()
17503 };
17504
17505 let item2 = lsp::CompletionItem {
17506 label: "other".to_string(),
17507 filter_text: Some("other".to_string()),
17508 detail: None,
17509 documentation: None,
17510 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17511 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17512 new_text: ".other".to_string(),
17513 })),
17514 ..lsp::CompletionItem::default()
17515 };
17516
17517 let item1 = item1.clone();
17518 cx.set_request_handler::<lsp::request::Completion, _, _>({
17519 let item1 = item1.clone();
17520 move |_, _, _| {
17521 let item1 = item1.clone();
17522 let item2 = item2.clone();
17523 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17524 }
17525 })
17526 .next()
17527 .await;
17528
17529 cx.condition(|editor, _| editor.context_menu_visible())
17530 .await;
17531 cx.update_editor(|editor, _, _| {
17532 let context_menu = editor.context_menu.borrow_mut();
17533 let context_menu = context_menu
17534 .as_ref()
17535 .expect("Should have the context menu deployed");
17536 match context_menu {
17537 CodeContextMenu::Completions(completions_menu) => {
17538 let completions = completions_menu.completions.borrow_mut();
17539 assert_eq!(
17540 completions
17541 .iter()
17542 .map(|completion| &completion.label.text)
17543 .collect::<Vec<_>>(),
17544 vec!["method id()", "other"]
17545 )
17546 }
17547 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17548 }
17549 });
17550
17551 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17552 let item1 = item1.clone();
17553 move |_, item_to_resolve, _| {
17554 let item1 = item1.clone();
17555 async move {
17556 if item1 == item_to_resolve {
17557 Ok(lsp::CompletionItem {
17558 label: "method id()".to_string(),
17559 filter_text: Some("id".to_string()),
17560 detail: Some("Now resolved!".to_string()),
17561 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17562 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17563 range: lsp::Range::new(
17564 lsp::Position::new(0, 22),
17565 lsp::Position::new(0, 22),
17566 ),
17567 new_text: ".id".to_string(),
17568 })),
17569 ..lsp::CompletionItem::default()
17570 })
17571 } else {
17572 Ok(item_to_resolve)
17573 }
17574 }
17575 }
17576 })
17577 .next()
17578 .await
17579 .unwrap();
17580 cx.run_until_parked();
17581
17582 cx.update_editor(|editor, window, cx| {
17583 editor.context_menu_next(&Default::default(), window, cx);
17584 });
17585
17586 cx.update_editor(|editor, _, _| {
17587 let context_menu = editor.context_menu.borrow_mut();
17588 let context_menu = context_menu
17589 .as_ref()
17590 .expect("Should have the context menu deployed");
17591 match context_menu {
17592 CodeContextMenu::Completions(completions_menu) => {
17593 let completions = completions_menu.completions.borrow_mut();
17594 assert_eq!(
17595 completions
17596 .iter()
17597 .map(|completion| &completion.label.text)
17598 .collect::<Vec<_>>(),
17599 vec!["method id() Now resolved!", "other"],
17600 "Should update first completion label, but not second as the filter text did not match."
17601 );
17602 }
17603 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17604 }
17605 });
17606}
17607
17608#[gpui::test]
17609async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17610 init_test(cx, |_| {});
17611 let mut cx = EditorLspTestContext::new_rust(
17612 lsp::ServerCapabilities {
17613 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17614 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17615 completion_provider: Some(lsp::CompletionOptions {
17616 resolve_provider: Some(true),
17617 ..Default::default()
17618 }),
17619 ..Default::default()
17620 },
17621 cx,
17622 )
17623 .await;
17624 cx.set_state(indoc! {"
17625 struct TestStruct {
17626 field: i32
17627 }
17628
17629 fn mainˇ() {
17630 let unused_var = 42;
17631 let test_struct = TestStruct { field: 42 };
17632 }
17633 "});
17634 let symbol_range = cx.lsp_range(indoc! {"
17635 struct TestStruct {
17636 field: i32
17637 }
17638
17639 «fn main»() {
17640 let unused_var = 42;
17641 let test_struct = TestStruct { field: 42 };
17642 }
17643 "});
17644 let mut hover_requests =
17645 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17646 Ok(Some(lsp::Hover {
17647 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17648 kind: lsp::MarkupKind::Markdown,
17649 value: "Function documentation".to_string(),
17650 }),
17651 range: Some(symbol_range),
17652 }))
17653 });
17654
17655 // Case 1: Test that code action menu hide hover popover
17656 cx.dispatch_action(Hover);
17657 hover_requests.next().await;
17658 cx.condition(|editor, _| editor.hover_state.visible()).await;
17659 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17660 move |_, _, _| async move {
17661 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17662 lsp::CodeAction {
17663 title: "Remove unused variable".to_string(),
17664 kind: Some(CodeActionKind::QUICKFIX),
17665 edit: Some(lsp::WorkspaceEdit {
17666 changes: Some(
17667 [(
17668 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17669 vec![lsp::TextEdit {
17670 range: lsp::Range::new(
17671 lsp::Position::new(5, 4),
17672 lsp::Position::new(5, 27),
17673 ),
17674 new_text: "".to_string(),
17675 }],
17676 )]
17677 .into_iter()
17678 .collect(),
17679 ),
17680 ..Default::default()
17681 }),
17682 ..Default::default()
17683 },
17684 )]))
17685 },
17686 );
17687 cx.update_editor(|editor, window, cx| {
17688 editor.toggle_code_actions(
17689 &ToggleCodeActions {
17690 deployed_from: None,
17691 quick_launch: false,
17692 },
17693 window,
17694 cx,
17695 );
17696 });
17697 code_action_requests.next().await;
17698 cx.run_until_parked();
17699 cx.condition(|editor, _| editor.context_menu_visible())
17700 .await;
17701 cx.update_editor(|editor, _, _| {
17702 assert!(
17703 !editor.hover_state.visible(),
17704 "Hover popover should be hidden when code action menu is shown"
17705 );
17706 // Hide code actions
17707 editor.context_menu.take();
17708 });
17709
17710 // Case 2: Test that code completions hide hover popover
17711 cx.dispatch_action(Hover);
17712 hover_requests.next().await;
17713 cx.condition(|editor, _| editor.hover_state.visible()).await;
17714 let counter = Arc::new(AtomicUsize::new(0));
17715 let mut completion_requests =
17716 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17717 let counter = counter.clone();
17718 async move {
17719 counter.fetch_add(1, atomic::Ordering::Release);
17720 Ok(Some(lsp::CompletionResponse::Array(vec![
17721 lsp::CompletionItem {
17722 label: "main".into(),
17723 kind: Some(lsp::CompletionItemKind::FUNCTION),
17724 detail: Some("() -> ()".to_string()),
17725 ..Default::default()
17726 },
17727 lsp::CompletionItem {
17728 label: "TestStruct".into(),
17729 kind: Some(lsp::CompletionItemKind::STRUCT),
17730 detail: Some("struct TestStruct".to_string()),
17731 ..Default::default()
17732 },
17733 ])))
17734 }
17735 });
17736 cx.update_editor(|editor, window, cx| {
17737 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17738 });
17739 completion_requests.next().await;
17740 cx.condition(|editor, _| editor.context_menu_visible())
17741 .await;
17742 cx.update_editor(|editor, _, _| {
17743 assert!(
17744 !editor.hover_state.visible(),
17745 "Hover popover should be hidden when completion menu is shown"
17746 );
17747 });
17748}
17749
17750#[gpui::test]
17751async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17752 init_test(cx, |_| {});
17753
17754 let mut cx = EditorLspTestContext::new_rust(
17755 lsp::ServerCapabilities {
17756 completion_provider: Some(lsp::CompletionOptions {
17757 trigger_characters: Some(vec![".".to_string()]),
17758 resolve_provider: Some(true),
17759 ..Default::default()
17760 }),
17761 ..Default::default()
17762 },
17763 cx,
17764 )
17765 .await;
17766
17767 cx.set_state("fn main() { let a = 2ˇ; }");
17768 cx.simulate_keystroke(".");
17769
17770 let unresolved_item_1 = lsp::CompletionItem {
17771 label: "id".to_string(),
17772 filter_text: Some("id".to_string()),
17773 detail: None,
17774 documentation: None,
17775 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17776 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17777 new_text: ".id".to_string(),
17778 })),
17779 ..lsp::CompletionItem::default()
17780 };
17781 let resolved_item_1 = lsp::CompletionItem {
17782 additional_text_edits: Some(vec![lsp::TextEdit {
17783 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17784 new_text: "!!".to_string(),
17785 }]),
17786 ..unresolved_item_1.clone()
17787 };
17788 let unresolved_item_2 = lsp::CompletionItem {
17789 label: "other".to_string(),
17790 filter_text: Some("other".to_string()),
17791 detail: None,
17792 documentation: None,
17793 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17794 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17795 new_text: ".other".to_string(),
17796 })),
17797 ..lsp::CompletionItem::default()
17798 };
17799 let resolved_item_2 = lsp::CompletionItem {
17800 additional_text_edits: Some(vec![lsp::TextEdit {
17801 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17802 new_text: "??".to_string(),
17803 }]),
17804 ..unresolved_item_2.clone()
17805 };
17806
17807 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17808 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17809 cx.lsp
17810 .server
17811 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17812 let unresolved_item_1 = unresolved_item_1.clone();
17813 let resolved_item_1 = resolved_item_1.clone();
17814 let unresolved_item_2 = unresolved_item_2.clone();
17815 let resolved_item_2 = resolved_item_2.clone();
17816 let resolve_requests_1 = resolve_requests_1.clone();
17817 let resolve_requests_2 = resolve_requests_2.clone();
17818 move |unresolved_request, _| {
17819 let unresolved_item_1 = unresolved_item_1.clone();
17820 let resolved_item_1 = resolved_item_1.clone();
17821 let unresolved_item_2 = unresolved_item_2.clone();
17822 let resolved_item_2 = resolved_item_2.clone();
17823 let resolve_requests_1 = resolve_requests_1.clone();
17824 let resolve_requests_2 = resolve_requests_2.clone();
17825 async move {
17826 if unresolved_request == unresolved_item_1 {
17827 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17828 Ok(resolved_item_1.clone())
17829 } else if unresolved_request == unresolved_item_2 {
17830 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17831 Ok(resolved_item_2.clone())
17832 } else {
17833 panic!("Unexpected completion item {unresolved_request:?}")
17834 }
17835 }
17836 }
17837 })
17838 .detach();
17839
17840 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17841 let unresolved_item_1 = unresolved_item_1.clone();
17842 let unresolved_item_2 = unresolved_item_2.clone();
17843 async move {
17844 Ok(Some(lsp::CompletionResponse::Array(vec![
17845 unresolved_item_1,
17846 unresolved_item_2,
17847 ])))
17848 }
17849 })
17850 .next()
17851 .await;
17852
17853 cx.condition(|editor, _| editor.context_menu_visible())
17854 .await;
17855 cx.update_editor(|editor, _, _| {
17856 let context_menu = editor.context_menu.borrow_mut();
17857 let context_menu = context_menu
17858 .as_ref()
17859 .expect("Should have the context menu deployed");
17860 match context_menu {
17861 CodeContextMenu::Completions(completions_menu) => {
17862 let completions = completions_menu.completions.borrow_mut();
17863 assert_eq!(
17864 completions
17865 .iter()
17866 .map(|completion| &completion.label.text)
17867 .collect::<Vec<_>>(),
17868 vec!["id", "other"]
17869 )
17870 }
17871 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17872 }
17873 });
17874 cx.run_until_parked();
17875
17876 cx.update_editor(|editor, window, cx| {
17877 editor.context_menu_next(&ContextMenuNext, window, cx);
17878 });
17879 cx.run_until_parked();
17880 cx.update_editor(|editor, window, cx| {
17881 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17882 });
17883 cx.run_until_parked();
17884 cx.update_editor(|editor, window, cx| {
17885 editor.context_menu_next(&ContextMenuNext, window, cx);
17886 });
17887 cx.run_until_parked();
17888 cx.update_editor(|editor, window, cx| {
17889 editor
17890 .compose_completion(&ComposeCompletion::default(), window, cx)
17891 .expect("No task returned")
17892 })
17893 .await
17894 .expect("Completion failed");
17895 cx.run_until_parked();
17896
17897 cx.update_editor(|editor, _, cx| {
17898 assert_eq!(
17899 resolve_requests_1.load(atomic::Ordering::Acquire),
17900 1,
17901 "Should always resolve once despite multiple selections"
17902 );
17903 assert_eq!(
17904 resolve_requests_2.load(atomic::Ordering::Acquire),
17905 1,
17906 "Should always resolve once after multiple selections and applying the completion"
17907 );
17908 assert_eq!(
17909 editor.text(cx),
17910 "fn main() { let a = ??.other; }",
17911 "Should use resolved data when applying the completion"
17912 );
17913 });
17914}
17915
17916#[gpui::test]
17917async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17918 init_test(cx, |_| {});
17919
17920 let item_0 = lsp::CompletionItem {
17921 label: "abs".into(),
17922 insert_text: Some("abs".into()),
17923 data: Some(json!({ "very": "special"})),
17924 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17925 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17926 lsp::InsertReplaceEdit {
17927 new_text: "abs".to_string(),
17928 insert: lsp::Range::default(),
17929 replace: lsp::Range::default(),
17930 },
17931 )),
17932 ..lsp::CompletionItem::default()
17933 };
17934 let items = iter::once(item_0.clone())
17935 .chain((11..51).map(|i| lsp::CompletionItem {
17936 label: format!("item_{}", i),
17937 insert_text: Some(format!("item_{}", i)),
17938 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17939 ..lsp::CompletionItem::default()
17940 }))
17941 .collect::<Vec<_>>();
17942
17943 let default_commit_characters = vec!["?".to_string()];
17944 let default_data = json!({ "default": "data"});
17945 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17946 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17947 let default_edit_range = lsp::Range {
17948 start: lsp::Position {
17949 line: 0,
17950 character: 5,
17951 },
17952 end: lsp::Position {
17953 line: 0,
17954 character: 5,
17955 },
17956 };
17957
17958 let mut cx = EditorLspTestContext::new_rust(
17959 lsp::ServerCapabilities {
17960 completion_provider: Some(lsp::CompletionOptions {
17961 trigger_characters: Some(vec![".".to_string()]),
17962 resolve_provider: Some(true),
17963 ..Default::default()
17964 }),
17965 ..Default::default()
17966 },
17967 cx,
17968 )
17969 .await;
17970
17971 cx.set_state("fn main() { let a = 2ˇ; }");
17972 cx.simulate_keystroke(".");
17973
17974 let completion_data = default_data.clone();
17975 let completion_characters = default_commit_characters.clone();
17976 let completion_items = items.clone();
17977 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17978 let default_data = completion_data.clone();
17979 let default_commit_characters = completion_characters.clone();
17980 let items = completion_items.clone();
17981 async move {
17982 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17983 items,
17984 item_defaults: Some(lsp::CompletionListItemDefaults {
17985 data: Some(default_data.clone()),
17986 commit_characters: Some(default_commit_characters.clone()),
17987 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17988 default_edit_range,
17989 )),
17990 insert_text_format: Some(default_insert_text_format),
17991 insert_text_mode: Some(default_insert_text_mode),
17992 }),
17993 ..lsp::CompletionList::default()
17994 })))
17995 }
17996 })
17997 .next()
17998 .await;
17999
18000 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18001 cx.lsp
18002 .server
18003 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18004 let closure_resolved_items = resolved_items.clone();
18005 move |item_to_resolve, _| {
18006 let closure_resolved_items = closure_resolved_items.clone();
18007 async move {
18008 closure_resolved_items.lock().push(item_to_resolve.clone());
18009 Ok(item_to_resolve)
18010 }
18011 }
18012 })
18013 .detach();
18014
18015 cx.condition(|editor, _| editor.context_menu_visible())
18016 .await;
18017 cx.run_until_parked();
18018 cx.update_editor(|editor, _, _| {
18019 let menu = editor.context_menu.borrow_mut();
18020 match menu.as_ref().expect("should have the completions menu") {
18021 CodeContextMenu::Completions(completions_menu) => {
18022 assert_eq!(
18023 completions_menu
18024 .entries
18025 .borrow()
18026 .iter()
18027 .map(|mat| mat.string.clone())
18028 .collect::<Vec<String>>(),
18029 items
18030 .iter()
18031 .map(|completion| completion.label.clone())
18032 .collect::<Vec<String>>()
18033 );
18034 }
18035 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18036 }
18037 });
18038 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18039 // with 4 from the end.
18040 assert_eq!(
18041 *resolved_items.lock(),
18042 [&items[0..16], &items[items.len() - 4..items.len()]]
18043 .concat()
18044 .iter()
18045 .cloned()
18046 .map(|mut item| {
18047 if item.data.is_none() {
18048 item.data = Some(default_data.clone());
18049 }
18050 item
18051 })
18052 .collect::<Vec<lsp::CompletionItem>>(),
18053 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18054 );
18055 resolved_items.lock().clear();
18056
18057 cx.update_editor(|editor, window, cx| {
18058 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18059 });
18060 cx.run_until_parked();
18061 // Completions that have already been resolved are skipped.
18062 assert_eq!(
18063 *resolved_items.lock(),
18064 items[items.len() - 17..items.len() - 4]
18065 .iter()
18066 .cloned()
18067 .map(|mut item| {
18068 if item.data.is_none() {
18069 item.data = Some(default_data.clone());
18070 }
18071 item
18072 })
18073 .collect::<Vec<lsp::CompletionItem>>()
18074 );
18075 resolved_items.lock().clear();
18076}
18077
18078#[gpui::test]
18079async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18080 init_test(cx, |_| {});
18081
18082 let mut cx = EditorLspTestContext::new(
18083 Language::new(
18084 LanguageConfig {
18085 matcher: LanguageMatcher {
18086 path_suffixes: vec!["jsx".into()],
18087 ..Default::default()
18088 },
18089 overrides: [(
18090 "element".into(),
18091 LanguageConfigOverride {
18092 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18093 ..Default::default()
18094 },
18095 )]
18096 .into_iter()
18097 .collect(),
18098 ..Default::default()
18099 },
18100 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18101 )
18102 .with_override_query("(jsx_self_closing_element) @element")
18103 .unwrap(),
18104 lsp::ServerCapabilities {
18105 completion_provider: Some(lsp::CompletionOptions {
18106 trigger_characters: Some(vec![":".to_string()]),
18107 ..Default::default()
18108 }),
18109 ..Default::default()
18110 },
18111 cx,
18112 )
18113 .await;
18114
18115 cx.lsp
18116 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18117 Ok(Some(lsp::CompletionResponse::Array(vec![
18118 lsp::CompletionItem {
18119 label: "bg-blue".into(),
18120 ..Default::default()
18121 },
18122 lsp::CompletionItem {
18123 label: "bg-red".into(),
18124 ..Default::default()
18125 },
18126 lsp::CompletionItem {
18127 label: "bg-yellow".into(),
18128 ..Default::default()
18129 },
18130 ])))
18131 });
18132
18133 cx.set_state(r#"<p class="bgˇ" />"#);
18134
18135 // Trigger completion when typing a dash, because the dash is an extra
18136 // word character in the 'element' scope, which contains the cursor.
18137 cx.simulate_keystroke("-");
18138 cx.executor().run_until_parked();
18139 cx.update_editor(|editor, _, _| {
18140 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18141 {
18142 assert_eq!(
18143 completion_menu_entries(menu),
18144 &["bg-blue", "bg-red", "bg-yellow"]
18145 );
18146 } else {
18147 panic!("expected completion menu to be open");
18148 }
18149 });
18150
18151 cx.simulate_keystroke("l");
18152 cx.executor().run_until_parked();
18153 cx.update_editor(|editor, _, _| {
18154 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18155 {
18156 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18157 } else {
18158 panic!("expected completion menu to be open");
18159 }
18160 });
18161
18162 // When filtering completions, consider the character after the '-' to
18163 // be the start of a subword.
18164 cx.set_state(r#"<p class="yelˇ" />"#);
18165 cx.simulate_keystroke("l");
18166 cx.executor().run_until_parked();
18167 cx.update_editor(|editor, _, _| {
18168 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18169 {
18170 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18171 } else {
18172 panic!("expected completion menu to be open");
18173 }
18174 });
18175}
18176
18177fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18178 let entries = menu.entries.borrow();
18179 entries.iter().map(|mat| mat.string.clone()).collect()
18180}
18181
18182#[gpui::test]
18183async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18184 init_test(cx, |settings| {
18185 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
18186 });
18187
18188 let fs = FakeFs::new(cx.executor());
18189 fs.insert_file(path!("/file.ts"), Default::default()).await;
18190
18191 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18192 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18193
18194 language_registry.add(Arc::new(Language::new(
18195 LanguageConfig {
18196 name: "TypeScript".into(),
18197 matcher: LanguageMatcher {
18198 path_suffixes: vec!["ts".to_string()],
18199 ..Default::default()
18200 },
18201 ..Default::default()
18202 },
18203 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18204 )));
18205 update_test_language_settings(cx, |settings| {
18206 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18207 });
18208
18209 let test_plugin = "test_plugin";
18210 let _ = language_registry.register_fake_lsp(
18211 "TypeScript",
18212 FakeLspAdapter {
18213 prettier_plugins: vec![test_plugin],
18214 ..Default::default()
18215 },
18216 );
18217
18218 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18219 let buffer = project
18220 .update(cx, |project, cx| {
18221 project.open_local_buffer(path!("/file.ts"), cx)
18222 })
18223 .await
18224 .unwrap();
18225
18226 let buffer_text = "one\ntwo\nthree\n";
18227 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18228 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18229 editor.update_in(cx, |editor, window, cx| {
18230 editor.set_text(buffer_text, window, cx)
18231 });
18232
18233 editor
18234 .update_in(cx, |editor, window, cx| {
18235 editor.perform_format(
18236 project.clone(),
18237 FormatTrigger::Manual,
18238 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18239 window,
18240 cx,
18241 )
18242 })
18243 .unwrap()
18244 .await;
18245 assert_eq!(
18246 editor.update(cx, |editor, cx| editor.text(cx)),
18247 buffer_text.to_string() + prettier_format_suffix,
18248 "Test prettier formatting was not applied to the original buffer text",
18249 );
18250
18251 update_test_language_settings(cx, |settings| {
18252 settings.defaults.formatter = Some(FormatterList::default())
18253 });
18254 let format = editor.update_in(cx, |editor, window, cx| {
18255 editor.perform_format(
18256 project.clone(),
18257 FormatTrigger::Manual,
18258 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18259 window,
18260 cx,
18261 )
18262 });
18263 format.await.unwrap();
18264 assert_eq!(
18265 editor.update(cx, |editor, cx| editor.text(cx)),
18266 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18267 "Autoformatting (via test prettier) was not applied to the original buffer text",
18268 );
18269}
18270
18271#[gpui::test]
18272async fn test_addition_reverts(cx: &mut TestAppContext) {
18273 init_test(cx, |_| {});
18274 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18275 let base_text = indoc! {r#"
18276 struct Row;
18277 struct Row1;
18278 struct Row2;
18279
18280 struct Row4;
18281 struct Row5;
18282 struct Row6;
18283
18284 struct Row8;
18285 struct Row9;
18286 struct Row10;"#};
18287
18288 // When addition hunks are not adjacent to carets, no hunk revert is performed
18289 assert_hunk_revert(
18290 indoc! {r#"struct Row;
18291 struct Row1;
18292 struct Row1.1;
18293 struct Row1.2;
18294 struct Row2;ˇ
18295
18296 struct Row4;
18297 struct Row5;
18298 struct Row6;
18299
18300 struct Row8;
18301 ˇstruct Row9;
18302 struct Row9.1;
18303 struct Row9.2;
18304 struct Row9.3;
18305 struct Row10;"#},
18306 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18307 indoc! {r#"struct Row;
18308 struct Row1;
18309 struct Row1.1;
18310 struct Row1.2;
18311 struct Row2;ˇ
18312
18313 struct Row4;
18314 struct Row5;
18315 struct Row6;
18316
18317 struct Row8;
18318 ˇstruct Row9;
18319 struct Row9.1;
18320 struct Row9.2;
18321 struct Row9.3;
18322 struct Row10;"#},
18323 base_text,
18324 &mut cx,
18325 );
18326 // Same for selections
18327 assert_hunk_revert(
18328 indoc! {r#"struct Row;
18329 struct Row1;
18330 struct Row2;
18331 struct Row2.1;
18332 struct Row2.2;
18333 «ˇ
18334 struct Row4;
18335 struct» Row5;
18336 «struct Row6;
18337 ˇ»
18338 struct Row9.1;
18339 struct Row9.2;
18340 struct Row9.3;
18341 struct Row8;
18342 struct Row9;
18343 struct Row10;"#},
18344 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18345 indoc! {r#"struct Row;
18346 struct Row1;
18347 struct Row2;
18348 struct Row2.1;
18349 struct Row2.2;
18350 «ˇ
18351 struct Row4;
18352 struct» Row5;
18353 «struct Row6;
18354 ˇ»
18355 struct Row9.1;
18356 struct Row9.2;
18357 struct Row9.3;
18358 struct Row8;
18359 struct Row9;
18360 struct Row10;"#},
18361 base_text,
18362 &mut cx,
18363 );
18364
18365 // When carets and selections intersect the addition hunks, those are reverted.
18366 // Adjacent carets got merged.
18367 assert_hunk_revert(
18368 indoc! {r#"struct Row;
18369 ˇ// something on the top
18370 struct Row1;
18371 struct Row2;
18372 struct Roˇw3.1;
18373 struct Row2.2;
18374 struct Row2.3;ˇ
18375
18376 struct Row4;
18377 struct ˇRow5.1;
18378 struct Row5.2;
18379 struct «Rowˇ»5.3;
18380 struct Row5;
18381 struct Row6;
18382 ˇ
18383 struct Row9.1;
18384 struct «Rowˇ»9.2;
18385 struct «ˇRow»9.3;
18386 struct Row8;
18387 struct Row9;
18388 «ˇ// something on bottom»
18389 struct Row10;"#},
18390 vec![
18391 DiffHunkStatusKind::Added,
18392 DiffHunkStatusKind::Added,
18393 DiffHunkStatusKind::Added,
18394 DiffHunkStatusKind::Added,
18395 DiffHunkStatusKind::Added,
18396 ],
18397 indoc! {r#"struct Row;
18398 ˇstruct Row1;
18399 struct Row2;
18400 ˇ
18401 struct Row4;
18402 ˇstruct Row5;
18403 struct Row6;
18404 ˇ
18405 ˇstruct Row8;
18406 struct Row9;
18407 ˇstruct Row10;"#},
18408 base_text,
18409 &mut cx,
18410 );
18411}
18412
18413#[gpui::test]
18414async fn test_modification_reverts(cx: &mut TestAppContext) {
18415 init_test(cx, |_| {});
18416 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18417 let base_text = indoc! {r#"
18418 struct Row;
18419 struct Row1;
18420 struct Row2;
18421
18422 struct Row4;
18423 struct Row5;
18424 struct Row6;
18425
18426 struct Row8;
18427 struct Row9;
18428 struct Row10;"#};
18429
18430 // Modification hunks behave the same as the addition ones.
18431 assert_hunk_revert(
18432 indoc! {r#"struct Row;
18433 struct Row1;
18434 struct Row33;
18435 ˇ
18436 struct Row4;
18437 struct Row5;
18438 struct Row6;
18439 ˇ
18440 struct Row99;
18441 struct Row9;
18442 struct Row10;"#},
18443 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18444 indoc! {r#"struct Row;
18445 struct Row1;
18446 struct Row33;
18447 ˇ
18448 struct Row4;
18449 struct Row5;
18450 struct Row6;
18451 ˇ
18452 struct Row99;
18453 struct Row9;
18454 struct Row10;"#},
18455 base_text,
18456 &mut cx,
18457 );
18458 assert_hunk_revert(
18459 indoc! {r#"struct Row;
18460 struct Row1;
18461 struct Row33;
18462 «ˇ
18463 struct Row4;
18464 struct» Row5;
18465 «struct Row6;
18466 ˇ»
18467 struct Row99;
18468 struct Row9;
18469 struct Row10;"#},
18470 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18471 indoc! {r#"struct Row;
18472 struct Row1;
18473 struct Row33;
18474 «ˇ
18475 struct Row4;
18476 struct» Row5;
18477 «struct Row6;
18478 ˇ»
18479 struct Row99;
18480 struct Row9;
18481 struct Row10;"#},
18482 base_text,
18483 &mut cx,
18484 );
18485
18486 assert_hunk_revert(
18487 indoc! {r#"ˇstruct Row1.1;
18488 struct Row1;
18489 «ˇstr»uct Row22;
18490
18491 struct ˇRow44;
18492 struct Row5;
18493 struct «Rˇ»ow66;ˇ
18494
18495 «struˇ»ct Row88;
18496 struct Row9;
18497 struct Row1011;ˇ"#},
18498 vec![
18499 DiffHunkStatusKind::Modified,
18500 DiffHunkStatusKind::Modified,
18501 DiffHunkStatusKind::Modified,
18502 DiffHunkStatusKind::Modified,
18503 DiffHunkStatusKind::Modified,
18504 DiffHunkStatusKind::Modified,
18505 ],
18506 indoc! {r#"struct Row;
18507 ˇstruct Row1;
18508 struct Row2;
18509 ˇ
18510 struct Row4;
18511 ˇstruct Row5;
18512 struct Row6;
18513 ˇ
18514 struct Row8;
18515 ˇstruct Row9;
18516 struct Row10;ˇ"#},
18517 base_text,
18518 &mut cx,
18519 );
18520}
18521
18522#[gpui::test]
18523async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18524 init_test(cx, |_| {});
18525 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18526 let base_text = indoc! {r#"
18527 one
18528
18529 two
18530 three
18531 "#};
18532
18533 cx.set_head_text(base_text);
18534 cx.set_state("\nˇ\n");
18535 cx.executor().run_until_parked();
18536 cx.update_editor(|editor, _window, cx| {
18537 editor.expand_selected_diff_hunks(cx);
18538 });
18539 cx.executor().run_until_parked();
18540 cx.update_editor(|editor, window, cx| {
18541 editor.backspace(&Default::default(), window, cx);
18542 });
18543 cx.run_until_parked();
18544 cx.assert_state_with_diff(
18545 indoc! {r#"
18546
18547 - two
18548 - threeˇ
18549 +
18550 "#}
18551 .to_string(),
18552 );
18553}
18554
18555#[gpui::test]
18556async fn test_deletion_reverts(cx: &mut TestAppContext) {
18557 init_test(cx, |_| {});
18558 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18559 let base_text = indoc! {r#"struct Row;
18560struct Row1;
18561struct Row2;
18562
18563struct Row4;
18564struct Row5;
18565struct Row6;
18566
18567struct Row8;
18568struct Row9;
18569struct Row10;"#};
18570
18571 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18572 assert_hunk_revert(
18573 indoc! {r#"struct Row;
18574 struct Row2;
18575
18576 ˇstruct Row4;
18577 struct Row5;
18578 struct Row6;
18579 ˇ
18580 struct Row8;
18581 struct Row10;"#},
18582 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18583 indoc! {r#"struct Row;
18584 struct Row2;
18585
18586 ˇstruct Row4;
18587 struct Row5;
18588 struct Row6;
18589 ˇ
18590 struct Row8;
18591 struct Row10;"#},
18592 base_text,
18593 &mut cx,
18594 );
18595 assert_hunk_revert(
18596 indoc! {r#"struct Row;
18597 struct Row2;
18598
18599 «ˇstruct Row4;
18600 struct» Row5;
18601 «struct Row6;
18602 ˇ»
18603 struct Row8;
18604 struct Row10;"#},
18605 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18606 indoc! {r#"struct Row;
18607 struct Row2;
18608
18609 «ˇstruct Row4;
18610 struct» Row5;
18611 «struct Row6;
18612 ˇ»
18613 struct Row8;
18614 struct Row10;"#},
18615 base_text,
18616 &mut cx,
18617 );
18618
18619 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18620 assert_hunk_revert(
18621 indoc! {r#"struct Row;
18622 ˇstruct Row2;
18623
18624 struct Row4;
18625 struct Row5;
18626 struct Row6;
18627
18628 struct Row8;ˇ
18629 struct Row10;"#},
18630 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18631 indoc! {r#"struct Row;
18632 struct Row1;
18633 ˇstruct Row2;
18634
18635 struct Row4;
18636 struct Row5;
18637 struct Row6;
18638
18639 struct Row8;ˇ
18640 struct Row9;
18641 struct Row10;"#},
18642 base_text,
18643 &mut cx,
18644 );
18645 assert_hunk_revert(
18646 indoc! {r#"struct Row;
18647 struct Row2«ˇ;
18648 struct Row4;
18649 struct» Row5;
18650 «struct Row6;
18651
18652 struct Row8;ˇ»
18653 struct Row10;"#},
18654 vec![
18655 DiffHunkStatusKind::Deleted,
18656 DiffHunkStatusKind::Deleted,
18657 DiffHunkStatusKind::Deleted,
18658 ],
18659 indoc! {r#"struct Row;
18660 struct Row1;
18661 struct Row2«ˇ;
18662
18663 struct Row4;
18664 struct» Row5;
18665 «struct Row6;
18666
18667 struct Row8;ˇ»
18668 struct Row9;
18669 struct Row10;"#},
18670 base_text,
18671 &mut cx,
18672 );
18673}
18674
18675#[gpui::test]
18676async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18677 init_test(cx, |_| {});
18678
18679 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18680 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18681 let base_text_3 =
18682 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18683
18684 let text_1 = edit_first_char_of_every_line(base_text_1);
18685 let text_2 = edit_first_char_of_every_line(base_text_2);
18686 let text_3 = edit_first_char_of_every_line(base_text_3);
18687
18688 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18689 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18690 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18691
18692 let multibuffer = cx.new(|cx| {
18693 let mut multibuffer = MultiBuffer::new(ReadWrite);
18694 multibuffer.push_excerpts(
18695 buffer_1.clone(),
18696 [
18697 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18698 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18699 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18700 ],
18701 cx,
18702 );
18703 multibuffer.push_excerpts(
18704 buffer_2.clone(),
18705 [
18706 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18707 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18708 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18709 ],
18710 cx,
18711 );
18712 multibuffer.push_excerpts(
18713 buffer_3.clone(),
18714 [
18715 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18716 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18717 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18718 ],
18719 cx,
18720 );
18721 multibuffer
18722 });
18723
18724 let fs = FakeFs::new(cx.executor());
18725 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18726 let (editor, cx) = cx
18727 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18728 editor.update_in(cx, |editor, _window, cx| {
18729 for (buffer, diff_base) in [
18730 (buffer_1.clone(), base_text_1),
18731 (buffer_2.clone(), base_text_2),
18732 (buffer_3.clone(), base_text_3),
18733 ] {
18734 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18735 editor
18736 .buffer
18737 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18738 }
18739 });
18740 cx.executor().run_until_parked();
18741
18742 editor.update_in(cx, |editor, window, cx| {
18743 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}");
18744 editor.select_all(&SelectAll, window, cx);
18745 editor.git_restore(&Default::default(), window, cx);
18746 });
18747 cx.executor().run_until_parked();
18748
18749 // When all ranges are selected, all buffer hunks are reverted.
18750 editor.update(cx, |editor, cx| {
18751 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");
18752 });
18753 buffer_1.update(cx, |buffer, _| {
18754 assert_eq!(buffer.text(), base_text_1);
18755 });
18756 buffer_2.update(cx, |buffer, _| {
18757 assert_eq!(buffer.text(), base_text_2);
18758 });
18759 buffer_3.update(cx, |buffer, _| {
18760 assert_eq!(buffer.text(), base_text_3);
18761 });
18762
18763 editor.update_in(cx, |editor, window, cx| {
18764 editor.undo(&Default::default(), window, cx);
18765 });
18766
18767 editor.update_in(cx, |editor, window, cx| {
18768 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18769 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18770 });
18771 editor.git_restore(&Default::default(), window, cx);
18772 });
18773
18774 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18775 // but not affect buffer_2 and its related excerpts.
18776 editor.update(cx, |editor, cx| {
18777 assert_eq!(
18778 editor.text(cx),
18779 "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}"
18780 );
18781 });
18782 buffer_1.update(cx, |buffer, _| {
18783 assert_eq!(buffer.text(), base_text_1);
18784 });
18785 buffer_2.update(cx, |buffer, _| {
18786 assert_eq!(
18787 buffer.text(),
18788 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18789 );
18790 });
18791 buffer_3.update(cx, |buffer, _| {
18792 assert_eq!(
18793 buffer.text(),
18794 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18795 );
18796 });
18797
18798 fn edit_first_char_of_every_line(text: &str) -> String {
18799 text.split('\n')
18800 .map(|line| format!("X{}", &line[1..]))
18801 .collect::<Vec<_>>()
18802 .join("\n")
18803 }
18804}
18805
18806#[gpui::test]
18807async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18808 init_test(cx, |_| {});
18809
18810 let cols = 4;
18811 let rows = 10;
18812 let sample_text_1 = sample_text(rows, cols, 'a');
18813 assert_eq!(
18814 sample_text_1,
18815 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18816 );
18817 let sample_text_2 = sample_text(rows, cols, 'l');
18818 assert_eq!(
18819 sample_text_2,
18820 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18821 );
18822 let sample_text_3 = sample_text(rows, cols, 'v');
18823 assert_eq!(
18824 sample_text_3,
18825 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18826 );
18827
18828 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18829 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18830 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18831
18832 let multi_buffer = cx.new(|cx| {
18833 let mut multibuffer = MultiBuffer::new(ReadWrite);
18834 multibuffer.push_excerpts(
18835 buffer_1.clone(),
18836 [
18837 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18838 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18839 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18840 ],
18841 cx,
18842 );
18843 multibuffer.push_excerpts(
18844 buffer_2.clone(),
18845 [
18846 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18847 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18848 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18849 ],
18850 cx,
18851 );
18852 multibuffer.push_excerpts(
18853 buffer_3.clone(),
18854 [
18855 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18856 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18857 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18858 ],
18859 cx,
18860 );
18861 multibuffer
18862 });
18863
18864 let fs = FakeFs::new(cx.executor());
18865 fs.insert_tree(
18866 "/a",
18867 json!({
18868 "main.rs": sample_text_1,
18869 "other.rs": sample_text_2,
18870 "lib.rs": sample_text_3,
18871 }),
18872 )
18873 .await;
18874 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18875 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18876 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18877 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18878 Editor::new(
18879 EditorMode::full(),
18880 multi_buffer,
18881 Some(project.clone()),
18882 window,
18883 cx,
18884 )
18885 });
18886 let multibuffer_item_id = workspace
18887 .update(cx, |workspace, window, cx| {
18888 assert!(
18889 workspace.active_item(cx).is_none(),
18890 "active item should be None before the first item is added"
18891 );
18892 workspace.add_item_to_active_pane(
18893 Box::new(multi_buffer_editor.clone()),
18894 None,
18895 true,
18896 window,
18897 cx,
18898 );
18899 let active_item = workspace
18900 .active_item(cx)
18901 .expect("should have an active item after adding the multi buffer");
18902 assert_eq!(
18903 active_item.buffer_kind(cx),
18904 ItemBufferKind::Multibuffer,
18905 "A multi buffer was expected to active after adding"
18906 );
18907 active_item.item_id()
18908 })
18909 .unwrap();
18910 cx.executor().run_until_parked();
18911
18912 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18913 editor.change_selections(
18914 SelectionEffects::scroll(Autoscroll::Next),
18915 window,
18916 cx,
18917 |s| s.select_ranges(Some(1..2)),
18918 );
18919 editor.open_excerpts(&OpenExcerpts, window, cx);
18920 });
18921 cx.executor().run_until_parked();
18922 let first_item_id = workspace
18923 .update(cx, |workspace, window, cx| {
18924 let active_item = workspace
18925 .active_item(cx)
18926 .expect("should have an active item after navigating into the 1st buffer");
18927 let first_item_id = active_item.item_id();
18928 assert_ne!(
18929 first_item_id, multibuffer_item_id,
18930 "Should navigate into the 1st buffer and activate it"
18931 );
18932 assert_eq!(
18933 active_item.buffer_kind(cx),
18934 ItemBufferKind::Singleton,
18935 "New active item should be a singleton buffer"
18936 );
18937 assert_eq!(
18938 active_item
18939 .act_as::<Editor>(cx)
18940 .expect("should have navigated into an editor for the 1st buffer")
18941 .read(cx)
18942 .text(cx),
18943 sample_text_1
18944 );
18945
18946 workspace
18947 .go_back(workspace.active_pane().downgrade(), window, cx)
18948 .detach_and_log_err(cx);
18949
18950 first_item_id
18951 })
18952 .unwrap();
18953 cx.executor().run_until_parked();
18954 workspace
18955 .update(cx, |workspace, _, cx| {
18956 let active_item = workspace
18957 .active_item(cx)
18958 .expect("should have an active item after navigating back");
18959 assert_eq!(
18960 active_item.item_id(),
18961 multibuffer_item_id,
18962 "Should navigate back to the multi buffer"
18963 );
18964 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
18965 })
18966 .unwrap();
18967
18968 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18969 editor.change_selections(
18970 SelectionEffects::scroll(Autoscroll::Next),
18971 window,
18972 cx,
18973 |s| s.select_ranges(Some(39..40)),
18974 );
18975 editor.open_excerpts(&OpenExcerpts, window, cx);
18976 });
18977 cx.executor().run_until_parked();
18978 let second_item_id = workspace
18979 .update(cx, |workspace, window, cx| {
18980 let active_item = workspace
18981 .active_item(cx)
18982 .expect("should have an active item after navigating into the 2nd buffer");
18983 let second_item_id = active_item.item_id();
18984 assert_ne!(
18985 second_item_id, multibuffer_item_id,
18986 "Should navigate away from the multibuffer"
18987 );
18988 assert_ne!(
18989 second_item_id, first_item_id,
18990 "Should navigate into the 2nd buffer and activate it"
18991 );
18992 assert_eq!(
18993 active_item.buffer_kind(cx),
18994 ItemBufferKind::Singleton,
18995 "New active item should be a singleton buffer"
18996 );
18997 assert_eq!(
18998 active_item
18999 .act_as::<Editor>(cx)
19000 .expect("should have navigated into an editor")
19001 .read(cx)
19002 .text(cx),
19003 sample_text_2
19004 );
19005
19006 workspace
19007 .go_back(workspace.active_pane().downgrade(), window, cx)
19008 .detach_and_log_err(cx);
19009
19010 second_item_id
19011 })
19012 .unwrap();
19013 cx.executor().run_until_parked();
19014 workspace
19015 .update(cx, |workspace, _, cx| {
19016 let active_item = workspace
19017 .active_item(cx)
19018 .expect("should have an active item after navigating back from the 2nd buffer");
19019 assert_eq!(
19020 active_item.item_id(),
19021 multibuffer_item_id,
19022 "Should navigate back from the 2nd buffer to the multi buffer"
19023 );
19024 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19025 })
19026 .unwrap();
19027
19028 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19029 editor.change_selections(
19030 SelectionEffects::scroll(Autoscroll::Next),
19031 window,
19032 cx,
19033 |s| s.select_ranges(Some(70..70)),
19034 );
19035 editor.open_excerpts(&OpenExcerpts, window, cx);
19036 });
19037 cx.executor().run_until_parked();
19038 workspace
19039 .update(cx, |workspace, window, cx| {
19040 let active_item = workspace
19041 .active_item(cx)
19042 .expect("should have an active item after navigating into the 3rd buffer");
19043 let third_item_id = active_item.item_id();
19044 assert_ne!(
19045 third_item_id, multibuffer_item_id,
19046 "Should navigate into the 3rd buffer and activate it"
19047 );
19048 assert_ne!(third_item_id, first_item_id);
19049 assert_ne!(third_item_id, second_item_id);
19050 assert_eq!(
19051 active_item.buffer_kind(cx),
19052 ItemBufferKind::Singleton,
19053 "New active item should be a singleton buffer"
19054 );
19055 assert_eq!(
19056 active_item
19057 .act_as::<Editor>(cx)
19058 .expect("should have navigated into an editor")
19059 .read(cx)
19060 .text(cx),
19061 sample_text_3
19062 );
19063
19064 workspace
19065 .go_back(workspace.active_pane().downgrade(), window, cx)
19066 .detach_and_log_err(cx);
19067 })
19068 .unwrap();
19069 cx.executor().run_until_parked();
19070 workspace
19071 .update(cx, |workspace, _, cx| {
19072 let active_item = workspace
19073 .active_item(cx)
19074 .expect("should have an active item after navigating back from the 3rd buffer");
19075 assert_eq!(
19076 active_item.item_id(),
19077 multibuffer_item_id,
19078 "Should navigate back from the 3rd buffer to the multi buffer"
19079 );
19080 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19081 })
19082 .unwrap();
19083}
19084
19085#[gpui::test]
19086async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19087 init_test(cx, |_| {});
19088
19089 let mut cx = EditorTestContext::new(cx).await;
19090
19091 let diff_base = r#"
19092 use some::mod;
19093
19094 const A: u32 = 42;
19095
19096 fn main() {
19097 println!("hello");
19098
19099 println!("world");
19100 }
19101 "#
19102 .unindent();
19103
19104 cx.set_state(
19105 &r#"
19106 use some::modified;
19107
19108 ˇ
19109 fn main() {
19110 println!("hello there");
19111
19112 println!("around the");
19113 println!("world");
19114 }
19115 "#
19116 .unindent(),
19117 );
19118
19119 cx.set_head_text(&diff_base);
19120 executor.run_until_parked();
19121
19122 cx.update_editor(|editor, window, cx| {
19123 editor.go_to_next_hunk(&GoToHunk, window, cx);
19124 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19125 });
19126 executor.run_until_parked();
19127 cx.assert_state_with_diff(
19128 r#"
19129 use some::modified;
19130
19131
19132 fn main() {
19133 - println!("hello");
19134 + ˇ println!("hello there");
19135
19136 println!("around the");
19137 println!("world");
19138 }
19139 "#
19140 .unindent(),
19141 );
19142
19143 cx.update_editor(|editor, window, cx| {
19144 for _ in 0..2 {
19145 editor.go_to_next_hunk(&GoToHunk, window, cx);
19146 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19147 }
19148 });
19149 executor.run_until_parked();
19150 cx.assert_state_with_diff(
19151 r#"
19152 - use some::mod;
19153 + ˇuse some::modified;
19154
19155
19156 fn main() {
19157 - println!("hello");
19158 + println!("hello there");
19159
19160 + println!("around the");
19161 println!("world");
19162 }
19163 "#
19164 .unindent(),
19165 );
19166
19167 cx.update_editor(|editor, window, cx| {
19168 editor.go_to_next_hunk(&GoToHunk, window, cx);
19169 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19170 });
19171 executor.run_until_parked();
19172 cx.assert_state_with_diff(
19173 r#"
19174 - use some::mod;
19175 + use some::modified;
19176
19177 - const A: u32 = 42;
19178 ˇ
19179 fn main() {
19180 - println!("hello");
19181 + println!("hello there");
19182
19183 + println!("around the");
19184 println!("world");
19185 }
19186 "#
19187 .unindent(),
19188 );
19189
19190 cx.update_editor(|editor, window, cx| {
19191 editor.cancel(&Cancel, window, cx);
19192 });
19193
19194 cx.assert_state_with_diff(
19195 r#"
19196 use some::modified;
19197
19198 ˇ
19199 fn main() {
19200 println!("hello there");
19201
19202 println!("around the");
19203 println!("world");
19204 }
19205 "#
19206 .unindent(),
19207 );
19208}
19209
19210#[gpui::test]
19211async fn test_diff_base_change_with_expanded_diff_hunks(
19212 executor: BackgroundExecutor,
19213 cx: &mut TestAppContext,
19214) {
19215 init_test(cx, |_| {});
19216
19217 let mut cx = EditorTestContext::new(cx).await;
19218
19219 let diff_base = r#"
19220 use some::mod1;
19221 use some::mod2;
19222
19223 const A: u32 = 42;
19224 const B: u32 = 42;
19225 const C: u32 = 42;
19226
19227 fn main() {
19228 println!("hello");
19229
19230 println!("world");
19231 }
19232 "#
19233 .unindent();
19234
19235 cx.set_state(
19236 &r#"
19237 use some::mod2;
19238
19239 const A: u32 = 42;
19240 const C: u32 = 42;
19241
19242 fn main(ˇ) {
19243 //println!("hello");
19244
19245 println!("world");
19246 //
19247 //
19248 }
19249 "#
19250 .unindent(),
19251 );
19252
19253 cx.set_head_text(&diff_base);
19254 executor.run_until_parked();
19255
19256 cx.update_editor(|editor, window, cx| {
19257 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19258 });
19259 executor.run_until_parked();
19260 cx.assert_state_with_diff(
19261 r#"
19262 - use some::mod1;
19263 use some::mod2;
19264
19265 const A: u32 = 42;
19266 - const B: u32 = 42;
19267 const C: u32 = 42;
19268
19269 fn main(ˇ) {
19270 - println!("hello");
19271 + //println!("hello");
19272
19273 println!("world");
19274 + //
19275 + //
19276 }
19277 "#
19278 .unindent(),
19279 );
19280
19281 cx.set_head_text("new diff base!");
19282 executor.run_until_parked();
19283 cx.assert_state_with_diff(
19284 r#"
19285 - new diff base!
19286 + use some::mod2;
19287 +
19288 + const A: u32 = 42;
19289 + const C: u32 = 42;
19290 +
19291 + fn main(ˇ) {
19292 + //println!("hello");
19293 +
19294 + println!("world");
19295 + //
19296 + //
19297 + }
19298 "#
19299 .unindent(),
19300 );
19301}
19302
19303#[gpui::test]
19304async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19305 init_test(cx, |_| {});
19306
19307 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19308 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19309 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19310 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19311 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19312 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19313
19314 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19315 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19316 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19317
19318 let multi_buffer = cx.new(|cx| {
19319 let mut multibuffer = MultiBuffer::new(ReadWrite);
19320 multibuffer.push_excerpts(
19321 buffer_1.clone(),
19322 [
19323 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19324 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19325 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19326 ],
19327 cx,
19328 );
19329 multibuffer.push_excerpts(
19330 buffer_2.clone(),
19331 [
19332 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19333 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19334 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19335 ],
19336 cx,
19337 );
19338 multibuffer.push_excerpts(
19339 buffer_3.clone(),
19340 [
19341 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19342 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19343 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19344 ],
19345 cx,
19346 );
19347 multibuffer
19348 });
19349
19350 let editor =
19351 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19352 editor
19353 .update(cx, |editor, _window, cx| {
19354 for (buffer, diff_base) in [
19355 (buffer_1.clone(), file_1_old),
19356 (buffer_2.clone(), file_2_old),
19357 (buffer_3.clone(), file_3_old),
19358 ] {
19359 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19360 editor
19361 .buffer
19362 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19363 }
19364 })
19365 .unwrap();
19366
19367 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19368 cx.run_until_parked();
19369
19370 cx.assert_editor_state(
19371 &"
19372 ˇaaa
19373 ccc
19374 ddd
19375
19376 ggg
19377 hhh
19378
19379
19380 lll
19381 mmm
19382 NNN
19383
19384 qqq
19385 rrr
19386
19387 uuu
19388 111
19389 222
19390 333
19391
19392 666
19393 777
19394
19395 000
19396 !!!"
19397 .unindent(),
19398 );
19399
19400 cx.update_editor(|editor, window, cx| {
19401 editor.select_all(&SelectAll, window, cx);
19402 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19403 });
19404 cx.executor().run_until_parked();
19405
19406 cx.assert_state_with_diff(
19407 "
19408 «aaa
19409 - bbb
19410 ccc
19411 ddd
19412
19413 ggg
19414 hhh
19415
19416
19417 lll
19418 mmm
19419 - nnn
19420 + NNN
19421
19422 qqq
19423 rrr
19424
19425 uuu
19426 111
19427 222
19428 333
19429
19430 + 666
19431 777
19432
19433 000
19434 !!!ˇ»"
19435 .unindent(),
19436 );
19437}
19438
19439#[gpui::test]
19440async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19441 init_test(cx, |_| {});
19442
19443 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19444 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19445
19446 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19447 let multi_buffer = cx.new(|cx| {
19448 let mut multibuffer = MultiBuffer::new(ReadWrite);
19449 multibuffer.push_excerpts(
19450 buffer.clone(),
19451 [
19452 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19453 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19454 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19455 ],
19456 cx,
19457 );
19458 multibuffer
19459 });
19460
19461 let editor =
19462 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19463 editor
19464 .update(cx, |editor, _window, cx| {
19465 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19466 editor
19467 .buffer
19468 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19469 })
19470 .unwrap();
19471
19472 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19473 cx.run_until_parked();
19474
19475 cx.update_editor(|editor, window, cx| {
19476 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19477 });
19478 cx.executor().run_until_parked();
19479
19480 // When the start of a hunk coincides with the start of its excerpt,
19481 // the hunk is expanded. When the start of a hunk is earlier than
19482 // the start of its excerpt, the hunk is not expanded.
19483 cx.assert_state_with_diff(
19484 "
19485 ˇaaa
19486 - bbb
19487 + BBB
19488
19489 - ddd
19490 - eee
19491 + DDD
19492 + EEE
19493 fff
19494
19495 iii
19496 "
19497 .unindent(),
19498 );
19499}
19500
19501#[gpui::test]
19502async fn test_edits_around_expanded_insertion_hunks(
19503 executor: BackgroundExecutor,
19504 cx: &mut TestAppContext,
19505) {
19506 init_test(cx, |_| {});
19507
19508 let mut cx = EditorTestContext::new(cx).await;
19509
19510 let diff_base = r#"
19511 use some::mod1;
19512 use some::mod2;
19513
19514 const A: u32 = 42;
19515
19516 fn main() {
19517 println!("hello");
19518
19519 println!("world");
19520 }
19521 "#
19522 .unindent();
19523 executor.run_until_parked();
19524 cx.set_state(
19525 &r#"
19526 use some::mod1;
19527 use some::mod2;
19528
19529 const A: u32 = 42;
19530 const B: u32 = 42;
19531 const C: u32 = 42;
19532 ˇ
19533
19534 fn main() {
19535 println!("hello");
19536
19537 println!("world");
19538 }
19539 "#
19540 .unindent(),
19541 );
19542
19543 cx.set_head_text(&diff_base);
19544 executor.run_until_parked();
19545
19546 cx.update_editor(|editor, window, cx| {
19547 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19548 });
19549 executor.run_until_parked();
19550
19551 cx.assert_state_with_diff(
19552 r#"
19553 use some::mod1;
19554 use some::mod2;
19555
19556 const A: u32 = 42;
19557 + const B: u32 = 42;
19558 + const C: u32 = 42;
19559 + ˇ
19560
19561 fn main() {
19562 println!("hello");
19563
19564 println!("world");
19565 }
19566 "#
19567 .unindent(),
19568 );
19569
19570 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19571 executor.run_until_parked();
19572
19573 cx.assert_state_with_diff(
19574 r#"
19575 use some::mod1;
19576 use some::mod2;
19577
19578 const A: u32 = 42;
19579 + const B: u32 = 42;
19580 + const C: u32 = 42;
19581 + const D: u32 = 42;
19582 + ˇ
19583
19584 fn main() {
19585 println!("hello");
19586
19587 println!("world");
19588 }
19589 "#
19590 .unindent(),
19591 );
19592
19593 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19594 executor.run_until_parked();
19595
19596 cx.assert_state_with_diff(
19597 r#"
19598 use some::mod1;
19599 use some::mod2;
19600
19601 const A: u32 = 42;
19602 + const B: u32 = 42;
19603 + const C: u32 = 42;
19604 + const D: u32 = 42;
19605 + const E: u32 = 42;
19606 + ˇ
19607
19608 fn main() {
19609 println!("hello");
19610
19611 println!("world");
19612 }
19613 "#
19614 .unindent(),
19615 );
19616
19617 cx.update_editor(|editor, window, cx| {
19618 editor.delete_line(&DeleteLine, window, cx);
19619 });
19620 executor.run_until_parked();
19621
19622 cx.assert_state_with_diff(
19623 r#"
19624 use some::mod1;
19625 use some::mod2;
19626
19627 const A: u32 = 42;
19628 + const B: u32 = 42;
19629 + const C: u32 = 42;
19630 + const D: u32 = 42;
19631 + const E: u32 = 42;
19632 ˇ
19633 fn main() {
19634 println!("hello");
19635
19636 println!("world");
19637 }
19638 "#
19639 .unindent(),
19640 );
19641
19642 cx.update_editor(|editor, window, cx| {
19643 editor.move_up(&MoveUp, window, cx);
19644 editor.delete_line(&DeleteLine, window, cx);
19645 editor.move_up(&MoveUp, window, cx);
19646 editor.delete_line(&DeleteLine, window, cx);
19647 editor.move_up(&MoveUp, window, cx);
19648 editor.delete_line(&DeleteLine, window, cx);
19649 });
19650 executor.run_until_parked();
19651 cx.assert_state_with_diff(
19652 r#"
19653 use some::mod1;
19654 use some::mod2;
19655
19656 const A: u32 = 42;
19657 + const B: u32 = 42;
19658 ˇ
19659 fn main() {
19660 println!("hello");
19661
19662 println!("world");
19663 }
19664 "#
19665 .unindent(),
19666 );
19667
19668 cx.update_editor(|editor, window, cx| {
19669 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19670 editor.delete_line(&DeleteLine, window, cx);
19671 });
19672 executor.run_until_parked();
19673 cx.assert_state_with_diff(
19674 r#"
19675 ˇ
19676 fn main() {
19677 println!("hello");
19678
19679 println!("world");
19680 }
19681 "#
19682 .unindent(),
19683 );
19684}
19685
19686#[gpui::test]
19687async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19688 init_test(cx, |_| {});
19689
19690 let mut cx = EditorTestContext::new(cx).await;
19691 cx.set_head_text(indoc! { "
19692 one
19693 two
19694 three
19695 four
19696 five
19697 "
19698 });
19699 cx.set_state(indoc! { "
19700 one
19701 ˇthree
19702 five
19703 "});
19704 cx.run_until_parked();
19705 cx.update_editor(|editor, window, cx| {
19706 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19707 });
19708 cx.assert_state_with_diff(
19709 indoc! { "
19710 one
19711 - two
19712 ˇthree
19713 - four
19714 five
19715 "}
19716 .to_string(),
19717 );
19718 cx.update_editor(|editor, window, cx| {
19719 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19720 });
19721
19722 cx.assert_state_with_diff(
19723 indoc! { "
19724 one
19725 ˇthree
19726 five
19727 "}
19728 .to_string(),
19729 );
19730
19731 cx.set_state(indoc! { "
19732 one
19733 ˇTWO
19734 three
19735 four
19736 five
19737 "});
19738 cx.run_until_parked();
19739 cx.update_editor(|editor, window, cx| {
19740 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19741 });
19742
19743 cx.assert_state_with_diff(
19744 indoc! { "
19745 one
19746 - two
19747 + ˇTWO
19748 three
19749 four
19750 five
19751 "}
19752 .to_string(),
19753 );
19754 cx.update_editor(|editor, window, cx| {
19755 editor.move_up(&Default::default(), window, cx);
19756 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19757 });
19758 cx.assert_state_with_diff(
19759 indoc! { "
19760 one
19761 ˇTWO
19762 three
19763 four
19764 five
19765 "}
19766 .to_string(),
19767 );
19768}
19769
19770#[gpui::test]
19771async fn test_edits_around_expanded_deletion_hunks(
19772 executor: BackgroundExecutor,
19773 cx: &mut TestAppContext,
19774) {
19775 init_test(cx, |_| {});
19776
19777 let mut cx = EditorTestContext::new(cx).await;
19778
19779 let diff_base = r#"
19780 use some::mod1;
19781 use some::mod2;
19782
19783 const A: u32 = 42;
19784 const B: u32 = 42;
19785 const C: u32 = 42;
19786
19787
19788 fn main() {
19789 println!("hello");
19790
19791 println!("world");
19792 }
19793 "#
19794 .unindent();
19795 executor.run_until_parked();
19796 cx.set_state(
19797 &r#"
19798 use some::mod1;
19799 use some::mod2;
19800
19801 ˇconst B: u32 = 42;
19802 const C: u32 = 42;
19803
19804
19805 fn main() {
19806 println!("hello");
19807
19808 println!("world");
19809 }
19810 "#
19811 .unindent(),
19812 );
19813
19814 cx.set_head_text(&diff_base);
19815 executor.run_until_parked();
19816
19817 cx.update_editor(|editor, window, cx| {
19818 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19819 });
19820 executor.run_until_parked();
19821
19822 cx.assert_state_with_diff(
19823 r#"
19824 use some::mod1;
19825 use some::mod2;
19826
19827 - const A: u32 = 42;
19828 ˇconst B: u32 = 42;
19829 const C: u32 = 42;
19830
19831
19832 fn main() {
19833 println!("hello");
19834
19835 println!("world");
19836 }
19837 "#
19838 .unindent(),
19839 );
19840
19841 cx.update_editor(|editor, window, cx| {
19842 editor.delete_line(&DeleteLine, window, cx);
19843 });
19844 executor.run_until_parked();
19845 cx.assert_state_with_diff(
19846 r#"
19847 use some::mod1;
19848 use some::mod2;
19849
19850 - const A: u32 = 42;
19851 - const B: u32 = 42;
19852 ˇconst C: u32 = 42;
19853
19854
19855 fn main() {
19856 println!("hello");
19857
19858 println!("world");
19859 }
19860 "#
19861 .unindent(),
19862 );
19863
19864 cx.update_editor(|editor, window, cx| {
19865 editor.delete_line(&DeleteLine, window, cx);
19866 });
19867 executor.run_until_parked();
19868 cx.assert_state_with_diff(
19869 r#"
19870 use some::mod1;
19871 use some::mod2;
19872
19873 - const A: u32 = 42;
19874 - const B: u32 = 42;
19875 - const C: u32 = 42;
19876 ˇ
19877
19878 fn main() {
19879 println!("hello");
19880
19881 println!("world");
19882 }
19883 "#
19884 .unindent(),
19885 );
19886
19887 cx.update_editor(|editor, window, cx| {
19888 editor.handle_input("replacement", window, cx);
19889 });
19890 executor.run_until_parked();
19891 cx.assert_state_with_diff(
19892 r#"
19893 use some::mod1;
19894 use some::mod2;
19895
19896 - const A: u32 = 42;
19897 - const B: u32 = 42;
19898 - const C: u32 = 42;
19899 -
19900 + replacementˇ
19901
19902 fn main() {
19903 println!("hello");
19904
19905 println!("world");
19906 }
19907 "#
19908 .unindent(),
19909 );
19910}
19911
19912#[gpui::test]
19913async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19914 init_test(cx, |_| {});
19915
19916 let mut cx = EditorTestContext::new(cx).await;
19917
19918 let base_text = r#"
19919 one
19920 two
19921 three
19922 four
19923 five
19924 "#
19925 .unindent();
19926 executor.run_until_parked();
19927 cx.set_state(
19928 &r#"
19929 one
19930 two
19931 fˇour
19932 five
19933 "#
19934 .unindent(),
19935 );
19936
19937 cx.set_head_text(&base_text);
19938 executor.run_until_parked();
19939
19940 cx.update_editor(|editor, window, cx| {
19941 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19942 });
19943 executor.run_until_parked();
19944
19945 cx.assert_state_with_diff(
19946 r#"
19947 one
19948 two
19949 - three
19950 fˇour
19951 five
19952 "#
19953 .unindent(),
19954 );
19955
19956 cx.update_editor(|editor, window, cx| {
19957 editor.backspace(&Backspace, window, cx);
19958 editor.backspace(&Backspace, window, cx);
19959 });
19960 executor.run_until_parked();
19961 cx.assert_state_with_diff(
19962 r#"
19963 one
19964 two
19965 - threeˇ
19966 - four
19967 + our
19968 five
19969 "#
19970 .unindent(),
19971 );
19972}
19973
19974#[gpui::test]
19975async fn test_edit_after_expanded_modification_hunk(
19976 executor: BackgroundExecutor,
19977 cx: &mut TestAppContext,
19978) {
19979 init_test(cx, |_| {});
19980
19981 let mut cx = EditorTestContext::new(cx).await;
19982
19983 let diff_base = r#"
19984 use some::mod1;
19985 use some::mod2;
19986
19987 const A: u32 = 42;
19988 const B: u32 = 42;
19989 const C: u32 = 42;
19990 const D: u32 = 42;
19991
19992
19993 fn main() {
19994 println!("hello");
19995
19996 println!("world");
19997 }"#
19998 .unindent();
19999
20000 cx.set_state(
20001 &r#"
20002 use some::mod1;
20003 use some::mod2;
20004
20005 const A: u32 = 42;
20006 const B: u32 = 42;
20007 const C: u32 = 43ˇ
20008 const D: u32 = 42;
20009
20010
20011 fn main() {
20012 println!("hello");
20013
20014 println!("world");
20015 }"#
20016 .unindent(),
20017 );
20018
20019 cx.set_head_text(&diff_base);
20020 executor.run_until_parked();
20021 cx.update_editor(|editor, window, cx| {
20022 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20023 });
20024 executor.run_until_parked();
20025
20026 cx.assert_state_with_diff(
20027 r#"
20028 use some::mod1;
20029 use some::mod2;
20030
20031 const A: u32 = 42;
20032 const B: u32 = 42;
20033 - const C: u32 = 42;
20034 + const C: u32 = 43ˇ
20035 const D: u32 = 42;
20036
20037
20038 fn main() {
20039 println!("hello");
20040
20041 println!("world");
20042 }"#
20043 .unindent(),
20044 );
20045
20046 cx.update_editor(|editor, window, cx| {
20047 editor.handle_input("\nnew_line\n", window, cx);
20048 });
20049 executor.run_until_parked();
20050
20051 cx.assert_state_with_diff(
20052 r#"
20053 use some::mod1;
20054 use some::mod2;
20055
20056 const A: u32 = 42;
20057 const B: u32 = 42;
20058 - const C: u32 = 42;
20059 + const C: u32 = 43
20060 + new_line
20061 + ˇ
20062 const D: u32 = 42;
20063
20064
20065 fn main() {
20066 println!("hello");
20067
20068 println!("world");
20069 }"#
20070 .unindent(),
20071 );
20072}
20073
20074#[gpui::test]
20075async fn test_stage_and_unstage_added_file_hunk(
20076 executor: BackgroundExecutor,
20077 cx: &mut TestAppContext,
20078) {
20079 init_test(cx, |_| {});
20080
20081 let mut cx = EditorTestContext::new(cx).await;
20082 cx.update_editor(|editor, _, cx| {
20083 editor.set_expand_all_diff_hunks(cx);
20084 });
20085
20086 let working_copy = r#"
20087 ˇfn main() {
20088 println!("hello, world!");
20089 }
20090 "#
20091 .unindent();
20092
20093 cx.set_state(&working_copy);
20094 executor.run_until_parked();
20095
20096 cx.assert_state_with_diff(
20097 r#"
20098 + ˇfn main() {
20099 + println!("hello, world!");
20100 + }
20101 "#
20102 .unindent(),
20103 );
20104 cx.assert_index_text(None);
20105
20106 cx.update_editor(|editor, window, cx| {
20107 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20108 });
20109 executor.run_until_parked();
20110 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20111 cx.assert_state_with_diff(
20112 r#"
20113 + ˇfn main() {
20114 + println!("hello, world!");
20115 + }
20116 "#
20117 .unindent(),
20118 );
20119
20120 cx.update_editor(|editor, window, cx| {
20121 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20122 });
20123 executor.run_until_parked();
20124 cx.assert_index_text(None);
20125}
20126
20127async fn setup_indent_guides_editor(
20128 text: &str,
20129 cx: &mut TestAppContext,
20130) -> (BufferId, EditorTestContext) {
20131 init_test(cx, |_| {});
20132
20133 let mut cx = EditorTestContext::new(cx).await;
20134
20135 let buffer_id = cx.update_editor(|editor, window, cx| {
20136 editor.set_text(text, window, cx);
20137 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20138
20139 buffer_ids[0]
20140 });
20141
20142 (buffer_id, cx)
20143}
20144
20145fn assert_indent_guides(
20146 range: Range<u32>,
20147 expected: Vec<IndentGuide>,
20148 active_indices: Option<Vec<usize>>,
20149 cx: &mut EditorTestContext,
20150) {
20151 let indent_guides = cx.update_editor(|editor, window, cx| {
20152 let snapshot = editor.snapshot(window, cx).display_snapshot;
20153 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20154 editor,
20155 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20156 true,
20157 &snapshot,
20158 cx,
20159 );
20160
20161 indent_guides.sort_by(|a, b| {
20162 a.depth.cmp(&b.depth).then(
20163 a.start_row
20164 .cmp(&b.start_row)
20165 .then(a.end_row.cmp(&b.end_row)),
20166 )
20167 });
20168 indent_guides
20169 });
20170
20171 if let Some(expected) = active_indices {
20172 let active_indices = cx.update_editor(|editor, window, cx| {
20173 let snapshot = editor.snapshot(window, cx).display_snapshot;
20174 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20175 });
20176
20177 assert_eq!(
20178 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20179 expected,
20180 "Active indent guide indices do not match"
20181 );
20182 }
20183
20184 assert_eq!(indent_guides, expected, "Indent guides do not match");
20185}
20186
20187fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20188 IndentGuide {
20189 buffer_id,
20190 start_row: MultiBufferRow(start_row),
20191 end_row: MultiBufferRow(end_row),
20192 depth,
20193 tab_size: 4,
20194 settings: IndentGuideSettings {
20195 enabled: true,
20196 line_width: 1,
20197 active_line_width: 1,
20198 coloring: IndentGuideColoring::default(),
20199 background_coloring: IndentGuideBackgroundColoring::default(),
20200 },
20201 }
20202}
20203
20204#[gpui::test]
20205async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20206 let (buffer_id, mut cx) = setup_indent_guides_editor(
20207 &"
20208 fn main() {
20209 let a = 1;
20210 }"
20211 .unindent(),
20212 cx,
20213 )
20214 .await;
20215
20216 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20217}
20218
20219#[gpui::test]
20220async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20221 let (buffer_id, mut cx) = setup_indent_guides_editor(
20222 &"
20223 fn main() {
20224 let a = 1;
20225 let b = 2;
20226 }"
20227 .unindent(),
20228 cx,
20229 )
20230 .await;
20231
20232 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20233}
20234
20235#[gpui::test]
20236async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20237 let (buffer_id, mut cx) = setup_indent_guides_editor(
20238 &"
20239 fn main() {
20240 let a = 1;
20241 if a == 3 {
20242 let b = 2;
20243 } else {
20244 let c = 3;
20245 }
20246 }"
20247 .unindent(),
20248 cx,
20249 )
20250 .await;
20251
20252 assert_indent_guides(
20253 0..8,
20254 vec![
20255 indent_guide(buffer_id, 1, 6, 0),
20256 indent_guide(buffer_id, 3, 3, 1),
20257 indent_guide(buffer_id, 5, 5, 1),
20258 ],
20259 None,
20260 &mut cx,
20261 );
20262}
20263
20264#[gpui::test]
20265async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20266 let (buffer_id, mut cx) = setup_indent_guides_editor(
20267 &"
20268 fn main() {
20269 let a = 1;
20270 let b = 2;
20271 let c = 3;
20272 }"
20273 .unindent(),
20274 cx,
20275 )
20276 .await;
20277
20278 assert_indent_guides(
20279 0..5,
20280 vec![
20281 indent_guide(buffer_id, 1, 3, 0),
20282 indent_guide(buffer_id, 2, 2, 1),
20283 ],
20284 None,
20285 &mut cx,
20286 );
20287}
20288
20289#[gpui::test]
20290async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20291 let (buffer_id, mut cx) = setup_indent_guides_editor(
20292 &"
20293 fn main() {
20294 let a = 1;
20295
20296 let c = 3;
20297 }"
20298 .unindent(),
20299 cx,
20300 )
20301 .await;
20302
20303 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20304}
20305
20306#[gpui::test]
20307async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20308 let (buffer_id, mut cx) = setup_indent_guides_editor(
20309 &"
20310 fn main() {
20311 let a = 1;
20312
20313 let c = 3;
20314
20315 if a == 3 {
20316 let b = 2;
20317 } else {
20318 let c = 3;
20319 }
20320 }"
20321 .unindent(),
20322 cx,
20323 )
20324 .await;
20325
20326 assert_indent_guides(
20327 0..11,
20328 vec![
20329 indent_guide(buffer_id, 1, 9, 0),
20330 indent_guide(buffer_id, 6, 6, 1),
20331 indent_guide(buffer_id, 8, 8, 1),
20332 ],
20333 None,
20334 &mut cx,
20335 );
20336}
20337
20338#[gpui::test]
20339async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20340 let (buffer_id, mut cx) = setup_indent_guides_editor(
20341 &"
20342 fn main() {
20343 let a = 1;
20344
20345 let c = 3;
20346
20347 if a == 3 {
20348 let b = 2;
20349 } else {
20350 let c = 3;
20351 }
20352 }"
20353 .unindent(),
20354 cx,
20355 )
20356 .await;
20357
20358 assert_indent_guides(
20359 1..11,
20360 vec![
20361 indent_guide(buffer_id, 1, 9, 0),
20362 indent_guide(buffer_id, 6, 6, 1),
20363 indent_guide(buffer_id, 8, 8, 1),
20364 ],
20365 None,
20366 &mut cx,
20367 );
20368}
20369
20370#[gpui::test]
20371async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20372 let (buffer_id, mut cx) = setup_indent_guides_editor(
20373 &"
20374 fn main() {
20375 let a = 1;
20376
20377 let c = 3;
20378
20379 if a == 3 {
20380 let b = 2;
20381 } else {
20382 let c = 3;
20383 }
20384 }"
20385 .unindent(),
20386 cx,
20387 )
20388 .await;
20389
20390 assert_indent_guides(
20391 1..10,
20392 vec![
20393 indent_guide(buffer_id, 1, 9, 0),
20394 indent_guide(buffer_id, 6, 6, 1),
20395 indent_guide(buffer_id, 8, 8, 1),
20396 ],
20397 None,
20398 &mut cx,
20399 );
20400}
20401
20402#[gpui::test]
20403async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20404 let (buffer_id, mut cx) = setup_indent_guides_editor(
20405 &"
20406 fn main() {
20407 if a {
20408 b(
20409 c,
20410 d,
20411 )
20412 } else {
20413 e(
20414 f
20415 )
20416 }
20417 }"
20418 .unindent(),
20419 cx,
20420 )
20421 .await;
20422
20423 assert_indent_guides(
20424 0..11,
20425 vec![
20426 indent_guide(buffer_id, 1, 10, 0),
20427 indent_guide(buffer_id, 2, 5, 1),
20428 indent_guide(buffer_id, 7, 9, 1),
20429 indent_guide(buffer_id, 3, 4, 2),
20430 indent_guide(buffer_id, 8, 8, 2),
20431 ],
20432 None,
20433 &mut cx,
20434 );
20435
20436 cx.update_editor(|editor, window, cx| {
20437 editor.fold_at(MultiBufferRow(2), window, cx);
20438 assert_eq!(
20439 editor.display_text(cx),
20440 "
20441 fn main() {
20442 if a {
20443 b(⋯
20444 )
20445 } else {
20446 e(
20447 f
20448 )
20449 }
20450 }"
20451 .unindent()
20452 );
20453 });
20454
20455 assert_indent_guides(
20456 0..11,
20457 vec![
20458 indent_guide(buffer_id, 1, 10, 0),
20459 indent_guide(buffer_id, 2, 5, 1),
20460 indent_guide(buffer_id, 7, 9, 1),
20461 indent_guide(buffer_id, 8, 8, 2),
20462 ],
20463 None,
20464 &mut cx,
20465 );
20466}
20467
20468#[gpui::test]
20469async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20470 let (buffer_id, mut cx) = setup_indent_guides_editor(
20471 &"
20472 block1
20473 block2
20474 block3
20475 block4
20476 block2
20477 block1
20478 block1"
20479 .unindent(),
20480 cx,
20481 )
20482 .await;
20483
20484 assert_indent_guides(
20485 1..10,
20486 vec![
20487 indent_guide(buffer_id, 1, 4, 0),
20488 indent_guide(buffer_id, 2, 3, 1),
20489 indent_guide(buffer_id, 3, 3, 2),
20490 ],
20491 None,
20492 &mut cx,
20493 );
20494}
20495
20496#[gpui::test]
20497async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20498 let (buffer_id, mut cx) = setup_indent_guides_editor(
20499 &"
20500 block1
20501 block2
20502 block3
20503
20504 block1
20505 block1"
20506 .unindent(),
20507 cx,
20508 )
20509 .await;
20510
20511 assert_indent_guides(
20512 0..6,
20513 vec![
20514 indent_guide(buffer_id, 1, 2, 0),
20515 indent_guide(buffer_id, 2, 2, 1),
20516 ],
20517 None,
20518 &mut cx,
20519 );
20520}
20521
20522#[gpui::test]
20523async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20524 let (buffer_id, mut cx) = setup_indent_guides_editor(
20525 &"
20526 function component() {
20527 \treturn (
20528 \t\t\t
20529 \t\t<div>
20530 \t\t\t<abc></abc>
20531 \t\t</div>
20532 \t)
20533 }"
20534 .unindent(),
20535 cx,
20536 )
20537 .await;
20538
20539 assert_indent_guides(
20540 0..8,
20541 vec![
20542 indent_guide(buffer_id, 1, 6, 0),
20543 indent_guide(buffer_id, 2, 5, 1),
20544 indent_guide(buffer_id, 4, 4, 2),
20545 ],
20546 None,
20547 &mut cx,
20548 );
20549}
20550
20551#[gpui::test]
20552async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20553 let (buffer_id, mut cx) = setup_indent_guides_editor(
20554 &"
20555 function component() {
20556 \treturn (
20557 \t
20558 \t\t<div>
20559 \t\t\t<abc></abc>
20560 \t\t</div>
20561 \t)
20562 }"
20563 .unindent(),
20564 cx,
20565 )
20566 .await;
20567
20568 assert_indent_guides(
20569 0..8,
20570 vec![
20571 indent_guide(buffer_id, 1, 6, 0),
20572 indent_guide(buffer_id, 2, 5, 1),
20573 indent_guide(buffer_id, 4, 4, 2),
20574 ],
20575 None,
20576 &mut cx,
20577 );
20578}
20579
20580#[gpui::test]
20581async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20582 let (buffer_id, mut cx) = setup_indent_guides_editor(
20583 &"
20584 block1
20585
20586
20587
20588 block2
20589 "
20590 .unindent(),
20591 cx,
20592 )
20593 .await;
20594
20595 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20596}
20597
20598#[gpui::test]
20599async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20600 let (buffer_id, mut cx) = setup_indent_guides_editor(
20601 &"
20602 def a:
20603 \tb = 3
20604 \tif True:
20605 \t\tc = 4
20606 \t\td = 5
20607 \tprint(b)
20608 "
20609 .unindent(),
20610 cx,
20611 )
20612 .await;
20613
20614 assert_indent_guides(
20615 0..6,
20616 vec![
20617 indent_guide(buffer_id, 1, 5, 0),
20618 indent_guide(buffer_id, 3, 4, 1),
20619 ],
20620 None,
20621 &mut cx,
20622 );
20623}
20624
20625#[gpui::test]
20626async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20627 let (buffer_id, mut cx) = setup_indent_guides_editor(
20628 &"
20629 fn main() {
20630 let a = 1;
20631 }"
20632 .unindent(),
20633 cx,
20634 )
20635 .await;
20636
20637 cx.update_editor(|editor, window, cx| {
20638 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20639 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20640 });
20641 });
20642
20643 assert_indent_guides(
20644 0..3,
20645 vec![indent_guide(buffer_id, 1, 1, 0)],
20646 Some(vec![0]),
20647 &mut cx,
20648 );
20649}
20650
20651#[gpui::test]
20652async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20653 let (buffer_id, mut cx) = setup_indent_guides_editor(
20654 &"
20655 fn main() {
20656 if 1 == 2 {
20657 let a = 1;
20658 }
20659 }"
20660 .unindent(),
20661 cx,
20662 )
20663 .await;
20664
20665 cx.update_editor(|editor, window, cx| {
20666 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20667 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20668 });
20669 });
20670
20671 assert_indent_guides(
20672 0..4,
20673 vec![
20674 indent_guide(buffer_id, 1, 3, 0),
20675 indent_guide(buffer_id, 2, 2, 1),
20676 ],
20677 Some(vec![1]),
20678 &mut cx,
20679 );
20680
20681 cx.update_editor(|editor, window, cx| {
20682 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20683 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20684 });
20685 });
20686
20687 assert_indent_guides(
20688 0..4,
20689 vec![
20690 indent_guide(buffer_id, 1, 3, 0),
20691 indent_guide(buffer_id, 2, 2, 1),
20692 ],
20693 Some(vec![1]),
20694 &mut cx,
20695 );
20696
20697 cx.update_editor(|editor, window, cx| {
20698 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20699 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20700 });
20701 });
20702
20703 assert_indent_guides(
20704 0..4,
20705 vec![
20706 indent_guide(buffer_id, 1, 3, 0),
20707 indent_guide(buffer_id, 2, 2, 1),
20708 ],
20709 Some(vec![0]),
20710 &mut cx,
20711 );
20712}
20713
20714#[gpui::test]
20715async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20716 let (buffer_id, mut cx) = setup_indent_guides_editor(
20717 &"
20718 fn main() {
20719 let a = 1;
20720
20721 let b = 2;
20722 }"
20723 .unindent(),
20724 cx,
20725 )
20726 .await;
20727
20728 cx.update_editor(|editor, window, cx| {
20729 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20730 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20731 });
20732 });
20733
20734 assert_indent_guides(
20735 0..5,
20736 vec![indent_guide(buffer_id, 1, 3, 0)],
20737 Some(vec![0]),
20738 &mut cx,
20739 );
20740}
20741
20742#[gpui::test]
20743async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20744 let (buffer_id, mut cx) = setup_indent_guides_editor(
20745 &"
20746 def m:
20747 a = 1
20748 pass"
20749 .unindent(),
20750 cx,
20751 )
20752 .await;
20753
20754 cx.update_editor(|editor, window, cx| {
20755 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20756 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20757 });
20758 });
20759
20760 assert_indent_guides(
20761 0..3,
20762 vec![indent_guide(buffer_id, 1, 2, 0)],
20763 Some(vec![0]),
20764 &mut cx,
20765 );
20766}
20767
20768#[gpui::test]
20769async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20770 init_test(cx, |_| {});
20771 let mut cx = EditorTestContext::new(cx).await;
20772 let text = indoc! {
20773 "
20774 impl A {
20775 fn b() {
20776 0;
20777 3;
20778 5;
20779 6;
20780 7;
20781 }
20782 }
20783 "
20784 };
20785 let base_text = indoc! {
20786 "
20787 impl A {
20788 fn b() {
20789 0;
20790 1;
20791 2;
20792 3;
20793 4;
20794 }
20795 fn c() {
20796 5;
20797 6;
20798 7;
20799 }
20800 }
20801 "
20802 };
20803
20804 cx.update_editor(|editor, window, cx| {
20805 editor.set_text(text, window, cx);
20806
20807 editor.buffer().update(cx, |multibuffer, cx| {
20808 let buffer = multibuffer.as_singleton().unwrap();
20809 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20810
20811 multibuffer.set_all_diff_hunks_expanded(cx);
20812 multibuffer.add_diff(diff, cx);
20813
20814 buffer.read(cx).remote_id()
20815 })
20816 });
20817 cx.run_until_parked();
20818
20819 cx.assert_state_with_diff(
20820 indoc! { "
20821 impl A {
20822 fn b() {
20823 0;
20824 - 1;
20825 - 2;
20826 3;
20827 - 4;
20828 - }
20829 - fn c() {
20830 5;
20831 6;
20832 7;
20833 }
20834 }
20835 ˇ"
20836 }
20837 .to_string(),
20838 );
20839
20840 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20841 editor
20842 .snapshot(window, cx)
20843 .buffer_snapshot()
20844 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20845 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20846 .collect::<Vec<_>>()
20847 });
20848 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20849 assert_eq!(
20850 actual_guides,
20851 vec![
20852 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20853 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20854 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20855 ]
20856 );
20857}
20858
20859#[gpui::test]
20860async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20861 init_test(cx, |_| {});
20862 let mut cx = EditorTestContext::new(cx).await;
20863
20864 let diff_base = r#"
20865 a
20866 b
20867 c
20868 "#
20869 .unindent();
20870
20871 cx.set_state(
20872 &r#"
20873 ˇA
20874 b
20875 C
20876 "#
20877 .unindent(),
20878 );
20879 cx.set_head_text(&diff_base);
20880 cx.update_editor(|editor, window, cx| {
20881 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20882 });
20883 executor.run_until_parked();
20884
20885 let both_hunks_expanded = r#"
20886 - a
20887 + ˇA
20888 b
20889 - c
20890 + C
20891 "#
20892 .unindent();
20893
20894 cx.assert_state_with_diff(both_hunks_expanded.clone());
20895
20896 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20897 let snapshot = editor.snapshot(window, cx);
20898 let hunks = editor
20899 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
20900 .collect::<Vec<_>>();
20901 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20902 let buffer_id = hunks[0].buffer_id;
20903 hunks
20904 .into_iter()
20905 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20906 .collect::<Vec<_>>()
20907 });
20908 assert_eq!(hunk_ranges.len(), 2);
20909
20910 cx.update_editor(|editor, _, cx| {
20911 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20912 });
20913 executor.run_until_parked();
20914
20915 let second_hunk_expanded = r#"
20916 ˇA
20917 b
20918 - c
20919 + C
20920 "#
20921 .unindent();
20922
20923 cx.assert_state_with_diff(second_hunk_expanded);
20924
20925 cx.update_editor(|editor, _, cx| {
20926 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20927 });
20928 executor.run_until_parked();
20929
20930 cx.assert_state_with_diff(both_hunks_expanded.clone());
20931
20932 cx.update_editor(|editor, _, cx| {
20933 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20934 });
20935 executor.run_until_parked();
20936
20937 let first_hunk_expanded = r#"
20938 - a
20939 + ˇA
20940 b
20941 C
20942 "#
20943 .unindent();
20944
20945 cx.assert_state_with_diff(first_hunk_expanded);
20946
20947 cx.update_editor(|editor, _, cx| {
20948 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20949 });
20950 executor.run_until_parked();
20951
20952 cx.assert_state_with_diff(both_hunks_expanded);
20953
20954 cx.set_state(
20955 &r#"
20956 ˇA
20957 b
20958 "#
20959 .unindent(),
20960 );
20961 cx.run_until_parked();
20962
20963 // TODO this cursor position seems bad
20964 cx.assert_state_with_diff(
20965 r#"
20966 - ˇa
20967 + A
20968 b
20969 "#
20970 .unindent(),
20971 );
20972
20973 cx.update_editor(|editor, window, cx| {
20974 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20975 });
20976
20977 cx.assert_state_with_diff(
20978 r#"
20979 - ˇa
20980 + A
20981 b
20982 - c
20983 "#
20984 .unindent(),
20985 );
20986
20987 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20988 let snapshot = editor.snapshot(window, cx);
20989 let hunks = editor
20990 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
20991 .collect::<Vec<_>>();
20992 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20993 let buffer_id = hunks[0].buffer_id;
20994 hunks
20995 .into_iter()
20996 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20997 .collect::<Vec<_>>()
20998 });
20999 assert_eq!(hunk_ranges.len(), 2);
21000
21001 cx.update_editor(|editor, _, cx| {
21002 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21003 });
21004 executor.run_until_parked();
21005
21006 cx.assert_state_with_diff(
21007 r#"
21008 - ˇa
21009 + A
21010 b
21011 "#
21012 .unindent(),
21013 );
21014}
21015
21016#[gpui::test]
21017async fn test_toggle_deletion_hunk_at_start_of_file(
21018 executor: BackgroundExecutor,
21019 cx: &mut TestAppContext,
21020) {
21021 init_test(cx, |_| {});
21022 let mut cx = EditorTestContext::new(cx).await;
21023
21024 let diff_base = r#"
21025 a
21026 b
21027 c
21028 "#
21029 .unindent();
21030
21031 cx.set_state(
21032 &r#"
21033 ˇb
21034 c
21035 "#
21036 .unindent(),
21037 );
21038 cx.set_head_text(&diff_base);
21039 cx.update_editor(|editor, window, cx| {
21040 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21041 });
21042 executor.run_until_parked();
21043
21044 let hunk_expanded = r#"
21045 - a
21046 ˇb
21047 c
21048 "#
21049 .unindent();
21050
21051 cx.assert_state_with_diff(hunk_expanded.clone());
21052
21053 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21054 let snapshot = editor.snapshot(window, cx);
21055 let hunks = editor
21056 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21057 .collect::<Vec<_>>();
21058 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21059 let buffer_id = hunks[0].buffer_id;
21060 hunks
21061 .into_iter()
21062 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21063 .collect::<Vec<_>>()
21064 });
21065 assert_eq!(hunk_ranges.len(), 1);
21066
21067 cx.update_editor(|editor, _, cx| {
21068 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21069 });
21070 executor.run_until_parked();
21071
21072 let hunk_collapsed = r#"
21073 ˇb
21074 c
21075 "#
21076 .unindent();
21077
21078 cx.assert_state_with_diff(hunk_collapsed);
21079
21080 cx.update_editor(|editor, _, cx| {
21081 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21082 });
21083 executor.run_until_parked();
21084
21085 cx.assert_state_with_diff(hunk_expanded);
21086}
21087
21088#[gpui::test]
21089async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21090 init_test(cx, |_| {});
21091
21092 let fs = FakeFs::new(cx.executor());
21093 fs.insert_tree(
21094 path!("/test"),
21095 json!({
21096 ".git": {},
21097 "file-1": "ONE\n",
21098 "file-2": "TWO\n",
21099 "file-3": "THREE\n",
21100 }),
21101 )
21102 .await;
21103
21104 fs.set_head_for_repo(
21105 path!("/test/.git").as_ref(),
21106 &[
21107 ("file-1", "one\n".into()),
21108 ("file-2", "two\n".into()),
21109 ("file-3", "three\n".into()),
21110 ],
21111 "deadbeef",
21112 );
21113
21114 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21115 let mut buffers = vec![];
21116 for i in 1..=3 {
21117 let buffer = project
21118 .update(cx, |project, cx| {
21119 let path = format!(path!("/test/file-{}"), i);
21120 project.open_local_buffer(path, cx)
21121 })
21122 .await
21123 .unwrap();
21124 buffers.push(buffer);
21125 }
21126
21127 let multibuffer = cx.new(|cx| {
21128 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21129 multibuffer.set_all_diff_hunks_expanded(cx);
21130 for buffer in &buffers {
21131 let snapshot = buffer.read(cx).snapshot();
21132 multibuffer.set_excerpts_for_path(
21133 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21134 buffer.clone(),
21135 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21136 2,
21137 cx,
21138 );
21139 }
21140 multibuffer
21141 });
21142
21143 let editor = cx.add_window(|window, cx| {
21144 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21145 });
21146 cx.run_until_parked();
21147
21148 let snapshot = editor
21149 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21150 .unwrap();
21151 let hunks = snapshot
21152 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21153 .map(|hunk| match hunk {
21154 DisplayDiffHunk::Unfolded {
21155 display_row_range, ..
21156 } => display_row_range,
21157 DisplayDiffHunk::Folded { .. } => unreachable!(),
21158 })
21159 .collect::<Vec<_>>();
21160 assert_eq!(
21161 hunks,
21162 [
21163 DisplayRow(2)..DisplayRow(4),
21164 DisplayRow(7)..DisplayRow(9),
21165 DisplayRow(12)..DisplayRow(14),
21166 ]
21167 );
21168}
21169
21170#[gpui::test]
21171async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21172 init_test(cx, |_| {});
21173
21174 let mut cx = EditorTestContext::new(cx).await;
21175 cx.set_head_text(indoc! { "
21176 one
21177 two
21178 three
21179 four
21180 five
21181 "
21182 });
21183 cx.set_index_text(indoc! { "
21184 one
21185 two
21186 three
21187 four
21188 five
21189 "
21190 });
21191 cx.set_state(indoc! {"
21192 one
21193 TWO
21194 ˇTHREE
21195 FOUR
21196 five
21197 "});
21198 cx.run_until_parked();
21199 cx.update_editor(|editor, window, cx| {
21200 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21201 });
21202 cx.run_until_parked();
21203 cx.assert_index_text(Some(indoc! {"
21204 one
21205 TWO
21206 THREE
21207 FOUR
21208 five
21209 "}));
21210 cx.set_state(indoc! { "
21211 one
21212 TWO
21213 ˇTHREE-HUNDRED
21214 FOUR
21215 five
21216 "});
21217 cx.run_until_parked();
21218 cx.update_editor(|editor, window, cx| {
21219 let snapshot = editor.snapshot(window, cx);
21220 let hunks = editor
21221 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21222 .collect::<Vec<_>>();
21223 assert_eq!(hunks.len(), 1);
21224 assert_eq!(
21225 hunks[0].status(),
21226 DiffHunkStatus {
21227 kind: DiffHunkStatusKind::Modified,
21228 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21229 }
21230 );
21231
21232 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21233 });
21234 cx.run_until_parked();
21235 cx.assert_index_text(Some(indoc! {"
21236 one
21237 TWO
21238 THREE-HUNDRED
21239 FOUR
21240 five
21241 "}));
21242}
21243
21244#[gpui::test]
21245fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21246 init_test(cx, |_| {});
21247
21248 let editor = cx.add_window(|window, cx| {
21249 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21250 build_editor(buffer, window, cx)
21251 });
21252
21253 let render_args = Arc::new(Mutex::new(None));
21254 let snapshot = editor
21255 .update(cx, |editor, window, cx| {
21256 let snapshot = editor.buffer().read(cx).snapshot(cx);
21257 let range =
21258 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21259
21260 struct RenderArgs {
21261 row: MultiBufferRow,
21262 folded: bool,
21263 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21264 }
21265
21266 let crease = Crease::inline(
21267 range,
21268 FoldPlaceholder::test(),
21269 {
21270 let toggle_callback = render_args.clone();
21271 move |row, folded, callback, _window, _cx| {
21272 *toggle_callback.lock() = Some(RenderArgs {
21273 row,
21274 folded,
21275 callback,
21276 });
21277 div()
21278 }
21279 },
21280 |_row, _folded, _window, _cx| div(),
21281 );
21282
21283 editor.insert_creases(Some(crease), cx);
21284 let snapshot = editor.snapshot(window, cx);
21285 let _div =
21286 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21287 snapshot
21288 })
21289 .unwrap();
21290
21291 let render_args = render_args.lock().take().unwrap();
21292 assert_eq!(render_args.row, MultiBufferRow(1));
21293 assert!(!render_args.folded);
21294 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21295
21296 cx.update_window(*editor, |_, window, cx| {
21297 (render_args.callback)(true, window, cx)
21298 })
21299 .unwrap();
21300 let snapshot = editor
21301 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21302 .unwrap();
21303 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21304
21305 cx.update_window(*editor, |_, window, cx| {
21306 (render_args.callback)(false, window, cx)
21307 })
21308 .unwrap();
21309 let snapshot = editor
21310 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21311 .unwrap();
21312 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21313}
21314
21315#[gpui::test]
21316async fn test_input_text(cx: &mut TestAppContext) {
21317 init_test(cx, |_| {});
21318 let mut cx = EditorTestContext::new(cx).await;
21319
21320 cx.set_state(
21321 &r#"ˇone
21322 two
21323
21324 three
21325 fourˇ
21326 five
21327
21328 siˇx"#
21329 .unindent(),
21330 );
21331
21332 cx.dispatch_action(HandleInput(String::new()));
21333 cx.assert_editor_state(
21334 &r#"ˇone
21335 two
21336
21337 three
21338 fourˇ
21339 five
21340
21341 siˇx"#
21342 .unindent(),
21343 );
21344
21345 cx.dispatch_action(HandleInput("AAAA".to_string()));
21346 cx.assert_editor_state(
21347 &r#"AAAAˇone
21348 two
21349
21350 three
21351 fourAAAAˇ
21352 five
21353
21354 siAAAAˇx"#
21355 .unindent(),
21356 );
21357}
21358
21359#[gpui::test]
21360async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21361 init_test(cx, |_| {});
21362
21363 let mut cx = EditorTestContext::new(cx).await;
21364 cx.set_state(
21365 r#"let foo = 1;
21366let foo = 2;
21367let foo = 3;
21368let fooˇ = 4;
21369let foo = 5;
21370let foo = 6;
21371let foo = 7;
21372let foo = 8;
21373let foo = 9;
21374let foo = 10;
21375let foo = 11;
21376let foo = 12;
21377let foo = 13;
21378let foo = 14;
21379let foo = 15;"#,
21380 );
21381
21382 cx.update_editor(|e, window, cx| {
21383 assert_eq!(
21384 e.next_scroll_position,
21385 NextScrollCursorCenterTopBottom::Center,
21386 "Default next scroll direction is center",
21387 );
21388
21389 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21390 assert_eq!(
21391 e.next_scroll_position,
21392 NextScrollCursorCenterTopBottom::Top,
21393 "After center, next scroll direction should be top",
21394 );
21395
21396 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21397 assert_eq!(
21398 e.next_scroll_position,
21399 NextScrollCursorCenterTopBottom::Bottom,
21400 "After top, next scroll direction should be bottom",
21401 );
21402
21403 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21404 assert_eq!(
21405 e.next_scroll_position,
21406 NextScrollCursorCenterTopBottom::Center,
21407 "After bottom, scrolling should start over",
21408 );
21409
21410 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21411 assert_eq!(
21412 e.next_scroll_position,
21413 NextScrollCursorCenterTopBottom::Top,
21414 "Scrolling continues if retriggered fast enough"
21415 );
21416 });
21417
21418 cx.executor()
21419 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21420 cx.executor().run_until_parked();
21421 cx.update_editor(|e, _, _| {
21422 assert_eq!(
21423 e.next_scroll_position,
21424 NextScrollCursorCenterTopBottom::Center,
21425 "If scrolling is not triggered fast enough, it should reset"
21426 );
21427 });
21428}
21429
21430#[gpui::test]
21431async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21432 init_test(cx, |_| {});
21433 let mut cx = EditorLspTestContext::new_rust(
21434 lsp::ServerCapabilities {
21435 definition_provider: Some(lsp::OneOf::Left(true)),
21436 references_provider: Some(lsp::OneOf::Left(true)),
21437 ..lsp::ServerCapabilities::default()
21438 },
21439 cx,
21440 )
21441 .await;
21442
21443 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21444 let go_to_definition = cx
21445 .lsp
21446 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21447 move |params, _| async move {
21448 if empty_go_to_definition {
21449 Ok(None)
21450 } else {
21451 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21452 uri: params.text_document_position_params.text_document.uri,
21453 range: lsp::Range::new(
21454 lsp::Position::new(4, 3),
21455 lsp::Position::new(4, 6),
21456 ),
21457 })))
21458 }
21459 },
21460 );
21461 let references = cx
21462 .lsp
21463 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21464 Ok(Some(vec![lsp::Location {
21465 uri: params.text_document_position.text_document.uri,
21466 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21467 }]))
21468 });
21469 (go_to_definition, references)
21470 };
21471
21472 cx.set_state(
21473 &r#"fn one() {
21474 let mut a = ˇtwo();
21475 }
21476
21477 fn two() {}"#
21478 .unindent(),
21479 );
21480 set_up_lsp_handlers(false, &mut cx);
21481 let navigated = cx
21482 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21483 .await
21484 .expect("Failed to navigate to definition");
21485 assert_eq!(
21486 navigated,
21487 Navigated::Yes,
21488 "Should have navigated to definition from the GetDefinition response"
21489 );
21490 cx.assert_editor_state(
21491 &r#"fn one() {
21492 let mut a = two();
21493 }
21494
21495 fn «twoˇ»() {}"#
21496 .unindent(),
21497 );
21498
21499 let editors = cx.update_workspace(|workspace, _, cx| {
21500 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21501 });
21502 cx.update_editor(|_, _, test_editor_cx| {
21503 assert_eq!(
21504 editors.len(),
21505 1,
21506 "Initially, only one, test, editor should be open in the workspace"
21507 );
21508 assert_eq!(
21509 test_editor_cx.entity(),
21510 editors.last().expect("Asserted len is 1").clone()
21511 );
21512 });
21513
21514 set_up_lsp_handlers(true, &mut cx);
21515 let navigated = cx
21516 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21517 .await
21518 .expect("Failed to navigate to lookup references");
21519 assert_eq!(
21520 navigated,
21521 Navigated::Yes,
21522 "Should have navigated to references as a fallback after empty GoToDefinition response"
21523 );
21524 // We should not change the selections in the existing file,
21525 // if opening another milti buffer with the references
21526 cx.assert_editor_state(
21527 &r#"fn one() {
21528 let mut a = two();
21529 }
21530
21531 fn «twoˇ»() {}"#
21532 .unindent(),
21533 );
21534 let editors = cx.update_workspace(|workspace, _, cx| {
21535 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21536 });
21537 cx.update_editor(|_, _, test_editor_cx| {
21538 assert_eq!(
21539 editors.len(),
21540 2,
21541 "After falling back to references search, we open a new editor with the results"
21542 );
21543 let references_fallback_text = editors
21544 .into_iter()
21545 .find(|new_editor| *new_editor != test_editor_cx.entity())
21546 .expect("Should have one non-test editor now")
21547 .read(test_editor_cx)
21548 .text(test_editor_cx);
21549 assert_eq!(
21550 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21551 "Should use the range from the references response and not the GoToDefinition one"
21552 );
21553 });
21554}
21555
21556#[gpui::test]
21557async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21558 init_test(cx, |_| {});
21559 cx.update(|cx| {
21560 let mut editor_settings = EditorSettings::get_global(cx).clone();
21561 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21562 EditorSettings::override_global(editor_settings, cx);
21563 });
21564 let mut cx = EditorLspTestContext::new_rust(
21565 lsp::ServerCapabilities {
21566 definition_provider: Some(lsp::OneOf::Left(true)),
21567 references_provider: Some(lsp::OneOf::Left(true)),
21568 ..lsp::ServerCapabilities::default()
21569 },
21570 cx,
21571 )
21572 .await;
21573 let original_state = r#"fn one() {
21574 let mut a = ˇtwo();
21575 }
21576
21577 fn two() {}"#
21578 .unindent();
21579 cx.set_state(&original_state);
21580
21581 let mut go_to_definition = cx
21582 .lsp
21583 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21584 move |_, _| async move { Ok(None) },
21585 );
21586 let _references = cx
21587 .lsp
21588 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21589 panic!("Should not call for references with no go to definition fallback")
21590 });
21591
21592 let navigated = cx
21593 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21594 .await
21595 .expect("Failed to navigate to lookup references");
21596 go_to_definition
21597 .next()
21598 .await
21599 .expect("Should have called the go_to_definition handler");
21600
21601 assert_eq!(
21602 navigated,
21603 Navigated::No,
21604 "Should have navigated to references as a fallback after empty GoToDefinition response"
21605 );
21606 cx.assert_editor_state(&original_state);
21607 let editors = cx.update_workspace(|workspace, _, cx| {
21608 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21609 });
21610 cx.update_editor(|_, _, _| {
21611 assert_eq!(
21612 editors.len(),
21613 1,
21614 "After unsuccessful fallback, no other editor should have been opened"
21615 );
21616 });
21617}
21618
21619#[gpui::test]
21620async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21621 init_test(cx, |_| {});
21622 let mut cx = EditorLspTestContext::new_rust(
21623 lsp::ServerCapabilities {
21624 references_provider: Some(lsp::OneOf::Left(true)),
21625 ..lsp::ServerCapabilities::default()
21626 },
21627 cx,
21628 )
21629 .await;
21630
21631 cx.set_state(
21632 &r#"
21633 fn one() {
21634 let mut a = two();
21635 }
21636
21637 fn ˇtwo() {}"#
21638 .unindent(),
21639 );
21640 cx.lsp
21641 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21642 Ok(Some(vec![
21643 lsp::Location {
21644 uri: params.text_document_position.text_document.uri.clone(),
21645 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21646 },
21647 lsp::Location {
21648 uri: params.text_document_position.text_document.uri,
21649 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21650 },
21651 ]))
21652 });
21653 let navigated = cx
21654 .update_editor(|editor, window, cx| {
21655 editor.find_all_references(&FindAllReferences, window, cx)
21656 })
21657 .unwrap()
21658 .await
21659 .expect("Failed to navigate to references");
21660 assert_eq!(
21661 navigated,
21662 Navigated::Yes,
21663 "Should have navigated to references from the FindAllReferences response"
21664 );
21665 cx.assert_editor_state(
21666 &r#"fn one() {
21667 let mut a = two();
21668 }
21669
21670 fn ˇtwo() {}"#
21671 .unindent(),
21672 );
21673
21674 let editors = cx.update_workspace(|workspace, _, cx| {
21675 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21676 });
21677 cx.update_editor(|_, _, _| {
21678 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21679 });
21680
21681 cx.set_state(
21682 &r#"fn one() {
21683 let mut a = ˇtwo();
21684 }
21685
21686 fn two() {}"#
21687 .unindent(),
21688 );
21689 let navigated = cx
21690 .update_editor(|editor, window, cx| {
21691 editor.find_all_references(&FindAllReferences, window, cx)
21692 })
21693 .unwrap()
21694 .await
21695 .expect("Failed to navigate to references");
21696 assert_eq!(
21697 navigated,
21698 Navigated::Yes,
21699 "Should have navigated to references from the FindAllReferences response"
21700 );
21701 cx.assert_editor_state(
21702 &r#"fn one() {
21703 let mut a = ˇtwo();
21704 }
21705
21706 fn two() {}"#
21707 .unindent(),
21708 );
21709 let editors = cx.update_workspace(|workspace, _, cx| {
21710 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21711 });
21712 cx.update_editor(|_, _, _| {
21713 assert_eq!(
21714 editors.len(),
21715 2,
21716 "should have re-used the previous multibuffer"
21717 );
21718 });
21719
21720 cx.set_state(
21721 &r#"fn one() {
21722 let mut a = ˇtwo();
21723 }
21724 fn three() {}
21725 fn two() {}"#
21726 .unindent(),
21727 );
21728 cx.lsp
21729 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21730 Ok(Some(vec![
21731 lsp::Location {
21732 uri: params.text_document_position.text_document.uri.clone(),
21733 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21734 },
21735 lsp::Location {
21736 uri: params.text_document_position.text_document.uri,
21737 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21738 },
21739 ]))
21740 });
21741 let navigated = cx
21742 .update_editor(|editor, window, cx| {
21743 editor.find_all_references(&FindAllReferences, window, cx)
21744 })
21745 .unwrap()
21746 .await
21747 .expect("Failed to navigate to references");
21748 assert_eq!(
21749 navigated,
21750 Navigated::Yes,
21751 "Should have navigated to references from the FindAllReferences response"
21752 );
21753 cx.assert_editor_state(
21754 &r#"fn one() {
21755 let mut a = ˇtwo();
21756 }
21757 fn three() {}
21758 fn two() {}"#
21759 .unindent(),
21760 );
21761 let editors = cx.update_workspace(|workspace, _, cx| {
21762 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21763 });
21764 cx.update_editor(|_, _, _| {
21765 assert_eq!(
21766 editors.len(),
21767 3,
21768 "should have used a new multibuffer as offsets changed"
21769 );
21770 });
21771}
21772#[gpui::test]
21773async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21774 init_test(cx, |_| {});
21775
21776 let language = Arc::new(Language::new(
21777 LanguageConfig::default(),
21778 Some(tree_sitter_rust::LANGUAGE.into()),
21779 ));
21780
21781 let text = r#"
21782 #[cfg(test)]
21783 mod tests() {
21784 #[test]
21785 fn runnable_1() {
21786 let a = 1;
21787 }
21788
21789 #[test]
21790 fn runnable_2() {
21791 let a = 1;
21792 let b = 2;
21793 }
21794 }
21795 "#
21796 .unindent();
21797
21798 let fs = FakeFs::new(cx.executor());
21799 fs.insert_file("/file.rs", Default::default()).await;
21800
21801 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21802 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21803 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21804 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21805 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21806
21807 let editor = cx.new_window_entity(|window, cx| {
21808 Editor::new(
21809 EditorMode::full(),
21810 multi_buffer,
21811 Some(project.clone()),
21812 window,
21813 cx,
21814 )
21815 });
21816
21817 editor.update_in(cx, |editor, window, cx| {
21818 let snapshot = editor.buffer().read(cx).snapshot(cx);
21819 editor.tasks.insert(
21820 (buffer.read(cx).remote_id(), 3),
21821 RunnableTasks {
21822 templates: vec![],
21823 offset: snapshot.anchor_before(43),
21824 column: 0,
21825 extra_variables: HashMap::default(),
21826 context_range: BufferOffset(43)..BufferOffset(85),
21827 },
21828 );
21829 editor.tasks.insert(
21830 (buffer.read(cx).remote_id(), 8),
21831 RunnableTasks {
21832 templates: vec![],
21833 offset: snapshot.anchor_before(86),
21834 column: 0,
21835 extra_variables: HashMap::default(),
21836 context_range: BufferOffset(86)..BufferOffset(191),
21837 },
21838 );
21839
21840 // Test finding task when cursor is inside function body
21841 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21842 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21843 });
21844 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21845 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21846
21847 // Test finding task when cursor is on function name
21848 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21849 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21850 });
21851 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21852 assert_eq!(row, 8, "Should find task when cursor is on function name");
21853 });
21854}
21855
21856#[gpui::test]
21857async fn test_folding_buffers(cx: &mut TestAppContext) {
21858 init_test(cx, |_| {});
21859
21860 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21861 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21862 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21863
21864 let fs = FakeFs::new(cx.executor());
21865 fs.insert_tree(
21866 path!("/a"),
21867 json!({
21868 "first.rs": sample_text_1,
21869 "second.rs": sample_text_2,
21870 "third.rs": sample_text_3,
21871 }),
21872 )
21873 .await;
21874 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21875 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21876 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21877 let worktree = project.update(cx, |project, cx| {
21878 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21879 assert_eq!(worktrees.len(), 1);
21880 worktrees.pop().unwrap()
21881 });
21882 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21883
21884 let buffer_1 = project
21885 .update(cx, |project, cx| {
21886 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
21887 })
21888 .await
21889 .unwrap();
21890 let buffer_2 = project
21891 .update(cx, |project, cx| {
21892 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
21893 })
21894 .await
21895 .unwrap();
21896 let buffer_3 = project
21897 .update(cx, |project, cx| {
21898 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
21899 })
21900 .await
21901 .unwrap();
21902
21903 let multi_buffer = cx.new(|cx| {
21904 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21905 multi_buffer.push_excerpts(
21906 buffer_1.clone(),
21907 [
21908 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21909 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21910 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21911 ],
21912 cx,
21913 );
21914 multi_buffer.push_excerpts(
21915 buffer_2.clone(),
21916 [
21917 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21918 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21919 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21920 ],
21921 cx,
21922 );
21923 multi_buffer.push_excerpts(
21924 buffer_3.clone(),
21925 [
21926 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21927 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21928 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21929 ],
21930 cx,
21931 );
21932 multi_buffer
21933 });
21934 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21935 Editor::new(
21936 EditorMode::full(),
21937 multi_buffer.clone(),
21938 Some(project.clone()),
21939 window,
21940 cx,
21941 )
21942 });
21943
21944 assert_eq!(
21945 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21946 "\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",
21947 );
21948
21949 multi_buffer_editor.update(cx, |editor, cx| {
21950 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21951 });
21952 assert_eq!(
21953 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21954 "\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",
21955 "After folding the first buffer, its text should not be displayed"
21956 );
21957
21958 multi_buffer_editor.update(cx, |editor, cx| {
21959 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21960 });
21961 assert_eq!(
21962 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21963 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21964 "After folding the second buffer, its text should not be displayed"
21965 );
21966
21967 multi_buffer_editor.update(cx, |editor, cx| {
21968 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21969 });
21970 assert_eq!(
21971 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21972 "\n\n\n\n\n",
21973 "After folding the third buffer, its text should not be displayed"
21974 );
21975
21976 // Emulate selection inside the fold logic, that should work
21977 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21978 editor
21979 .snapshot(window, cx)
21980 .next_line_boundary(Point::new(0, 4));
21981 });
21982
21983 multi_buffer_editor.update(cx, |editor, cx| {
21984 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21985 });
21986 assert_eq!(
21987 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21988 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21989 "After unfolding the second buffer, its text should be displayed"
21990 );
21991
21992 // Typing inside of buffer 1 causes that buffer to be unfolded.
21993 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21994 assert_eq!(
21995 multi_buffer
21996 .read(cx)
21997 .snapshot(cx)
21998 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21999 .collect::<String>(),
22000 "bbbb"
22001 );
22002 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22003 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22004 });
22005 editor.handle_input("B", window, cx);
22006 });
22007
22008 assert_eq!(
22009 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22010 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22011 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22012 );
22013
22014 multi_buffer_editor.update(cx, |editor, cx| {
22015 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22016 });
22017 assert_eq!(
22018 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22019 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22020 "After unfolding the all buffers, all original text should be displayed"
22021 );
22022}
22023
22024#[gpui::test]
22025async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22026 init_test(cx, |_| {});
22027
22028 let sample_text_1 = "1111\n2222\n3333".to_string();
22029 let sample_text_2 = "4444\n5555\n6666".to_string();
22030 let sample_text_3 = "7777\n8888\n9999".to_string();
22031
22032 let fs = FakeFs::new(cx.executor());
22033 fs.insert_tree(
22034 path!("/a"),
22035 json!({
22036 "first.rs": sample_text_1,
22037 "second.rs": sample_text_2,
22038 "third.rs": sample_text_3,
22039 }),
22040 )
22041 .await;
22042 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22043 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22044 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22045 let worktree = project.update(cx, |project, cx| {
22046 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22047 assert_eq!(worktrees.len(), 1);
22048 worktrees.pop().unwrap()
22049 });
22050 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22051
22052 let buffer_1 = project
22053 .update(cx, |project, cx| {
22054 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22055 })
22056 .await
22057 .unwrap();
22058 let buffer_2 = project
22059 .update(cx, |project, cx| {
22060 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22061 })
22062 .await
22063 .unwrap();
22064 let buffer_3 = project
22065 .update(cx, |project, cx| {
22066 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22067 })
22068 .await
22069 .unwrap();
22070
22071 let multi_buffer = cx.new(|cx| {
22072 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22073 multi_buffer.push_excerpts(
22074 buffer_1.clone(),
22075 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22076 cx,
22077 );
22078 multi_buffer.push_excerpts(
22079 buffer_2.clone(),
22080 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22081 cx,
22082 );
22083 multi_buffer.push_excerpts(
22084 buffer_3.clone(),
22085 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22086 cx,
22087 );
22088 multi_buffer
22089 });
22090
22091 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22092 Editor::new(
22093 EditorMode::full(),
22094 multi_buffer,
22095 Some(project.clone()),
22096 window,
22097 cx,
22098 )
22099 });
22100
22101 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22102 assert_eq!(
22103 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22104 full_text,
22105 );
22106
22107 multi_buffer_editor.update(cx, |editor, cx| {
22108 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22109 });
22110 assert_eq!(
22111 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22112 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22113 "After folding the first buffer, its text should not be displayed"
22114 );
22115
22116 multi_buffer_editor.update(cx, |editor, cx| {
22117 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22118 });
22119
22120 assert_eq!(
22121 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22122 "\n\n\n\n\n\n7777\n8888\n9999",
22123 "After folding the second buffer, its text should not be displayed"
22124 );
22125
22126 multi_buffer_editor.update(cx, |editor, cx| {
22127 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22128 });
22129 assert_eq!(
22130 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22131 "\n\n\n\n\n",
22132 "After folding the third buffer, its text should not be displayed"
22133 );
22134
22135 multi_buffer_editor.update(cx, |editor, cx| {
22136 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22137 });
22138 assert_eq!(
22139 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22140 "\n\n\n\n4444\n5555\n6666\n\n",
22141 "After unfolding the second buffer, its text should be displayed"
22142 );
22143
22144 multi_buffer_editor.update(cx, |editor, cx| {
22145 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22146 });
22147 assert_eq!(
22148 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22149 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22150 "After unfolding the first buffer, its text should be displayed"
22151 );
22152
22153 multi_buffer_editor.update(cx, |editor, cx| {
22154 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22155 });
22156 assert_eq!(
22157 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22158 full_text,
22159 "After unfolding all buffers, all original text should be displayed"
22160 );
22161}
22162
22163#[gpui::test]
22164async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22165 init_test(cx, |_| {});
22166
22167 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22168
22169 let fs = FakeFs::new(cx.executor());
22170 fs.insert_tree(
22171 path!("/a"),
22172 json!({
22173 "main.rs": sample_text,
22174 }),
22175 )
22176 .await;
22177 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22178 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22179 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22180 let worktree = project.update(cx, |project, cx| {
22181 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22182 assert_eq!(worktrees.len(), 1);
22183 worktrees.pop().unwrap()
22184 });
22185 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22186
22187 let buffer_1 = project
22188 .update(cx, |project, cx| {
22189 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22190 })
22191 .await
22192 .unwrap();
22193
22194 let multi_buffer = cx.new(|cx| {
22195 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22196 multi_buffer.push_excerpts(
22197 buffer_1.clone(),
22198 [ExcerptRange::new(
22199 Point::new(0, 0)
22200 ..Point::new(
22201 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22202 0,
22203 ),
22204 )],
22205 cx,
22206 );
22207 multi_buffer
22208 });
22209 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22210 Editor::new(
22211 EditorMode::full(),
22212 multi_buffer,
22213 Some(project.clone()),
22214 window,
22215 cx,
22216 )
22217 });
22218
22219 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22220 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22221 enum TestHighlight {}
22222 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22223 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22224 editor.highlight_text::<TestHighlight>(
22225 vec![highlight_range.clone()],
22226 HighlightStyle::color(Hsla::green()),
22227 cx,
22228 );
22229 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22230 s.select_ranges(Some(highlight_range))
22231 });
22232 });
22233
22234 let full_text = format!("\n\n{sample_text}");
22235 assert_eq!(
22236 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22237 full_text,
22238 );
22239}
22240
22241#[gpui::test]
22242async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22243 init_test(cx, |_| {});
22244 cx.update(|cx| {
22245 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22246 "keymaps/default-linux.json",
22247 cx,
22248 )
22249 .unwrap();
22250 cx.bind_keys(default_key_bindings);
22251 });
22252
22253 let (editor, cx) = cx.add_window_view(|window, cx| {
22254 let multi_buffer = MultiBuffer::build_multi(
22255 [
22256 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22257 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22258 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22259 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22260 ],
22261 cx,
22262 );
22263 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22264
22265 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22266 // fold all but the second buffer, so that we test navigating between two
22267 // adjacent folded buffers, as well as folded buffers at the start and
22268 // end the multibuffer
22269 editor.fold_buffer(buffer_ids[0], cx);
22270 editor.fold_buffer(buffer_ids[2], cx);
22271 editor.fold_buffer(buffer_ids[3], cx);
22272
22273 editor
22274 });
22275 cx.simulate_resize(size(px(1000.), px(1000.)));
22276
22277 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22278 cx.assert_excerpts_with_selections(indoc! {"
22279 [EXCERPT]
22280 ˇ[FOLDED]
22281 [EXCERPT]
22282 a1
22283 b1
22284 [EXCERPT]
22285 [FOLDED]
22286 [EXCERPT]
22287 [FOLDED]
22288 "
22289 });
22290 cx.simulate_keystroke("down");
22291 cx.assert_excerpts_with_selections(indoc! {"
22292 [EXCERPT]
22293 [FOLDED]
22294 [EXCERPT]
22295 ˇa1
22296 b1
22297 [EXCERPT]
22298 [FOLDED]
22299 [EXCERPT]
22300 [FOLDED]
22301 "
22302 });
22303 cx.simulate_keystroke("down");
22304 cx.assert_excerpts_with_selections(indoc! {"
22305 [EXCERPT]
22306 [FOLDED]
22307 [EXCERPT]
22308 a1
22309 ˇb1
22310 [EXCERPT]
22311 [FOLDED]
22312 [EXCERPT]
22313 [FOLDED]
22314 "
22315 });
22316 cx.simulate_keystroke("down");
22317 cx.assert_excerpts_with_selections(indoc! {"
22318 [EXCERPT]
22319 [FOLDED]
22320 [EXCERPT]
22321 a1
22322 b1
22323 ˇ[EXCERPT]
22324 [FOLDED]
22325 [EXCERPT]
22326 [FOLDED]
22327 "
22328 });
22329 cx.simulate_keystroke("down");
22330 cx.assert_excerpts_with_selections(indoc! {"
22331 [EXCERPT]
22332 [FOLDED]
22333 [EXCERPT]
22334 a1
22335 b1
22336 [EXCERPT]
22337 ˇ[FOLDED]
22338 [EXCERPT]
22339 [FOLDED]
22340 "
22341 });
22342 for _ in 0..5 {
22343 cx.simulate_keystroke("down");
22344 cx.assert_excerpts_with_selections(indoc! {"
22345 [EXCERPT]
22346 [FOLDED]
22347 [EXCERPT]
22348 a1
22349 b1
22350 [EXCERPT]
22351 [FOLDED]
22352 [EXCERPT]
22353 ˇ[FOLDED]
22354 "
22355 });
22356 }
22357
22358 cx.simulate_keystroke("up");
22359 cx.assert_excerpts_with_selections(indoc! {"
22360 [EXCERPT]
22361 [FOLDED]
22362 [EXCERPT]
22363 a1
22364 b1
22365 [EXCERPT]
22366 ˇ[FOLDED]
22367 [EXCERPT]
22368 [FOLDED]
22369 "
22370 });
22371 cx.simulate_keystroke("up");
22372 cx.assert_excerpts_with_selections(indoc! {"
22373 [EXCERPT]
22374 [FOLDED]
22375 [EXCERPT]
22376 a1
22377 b1
22378 ˇ[EXCERPT]
22379 [FOLDED]
22380 [EXCERPT]
22381 [FOLDED]
22382 "
22383 });
22384 cx.simulate_keystroke("up");
22385 cx.assert_excerpts_with_selections(indoc! {"
22386 [EXCERPT]
22387 [FOLDED]
22388 [EXCERPT]
22389 a1
22390 ˇb1
22391 [EXCERPT]
22392 [FOLDED]
22393 [EXCERPT]
22394 [FOLDED]
22395 "
22396 });
22397 cx.simulate_keystroke("up");
22398 cx.assert_excerpts_with_selections(indoc! {"
22399 [EXCERPT]
22400 [FOLDED]
22401 [EXCERPT]
22402 ˇa1
22403 b1
22404 [EXCERPT]
22405 [FOLDED]
22406 [EXCERPT]
22407 [FOLDED]
22408 "
22409 });
22410 for _ in 0..5 {
22411 cx.simulate_keystroke("up");
22412 cx.assert_excerpts_with_selections(indoc! {"
22413 [EXCERPT]
22414 ˇ[FOLDED]
22415 [EXCERPT]
22416 a1
22417 b1
22418 [EXCERPT]
22419 [FOLDED]
22420 [EXCERPT]
22421 [FOLDED]
22422 "
22423 });
22424 }
22425}
22426
22427#[gpui::test]
22428async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22429 init_test(cx, |_| {});
22430
22431 // Simple insertion
22432 assert_highlighted_edits(
22433 "Hello, world!",
22434 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22435 true,
22436 cx,
22437 |highlighted_edits, cx| {
22438 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22439 assert_eq!(highlighted_edits.highlights.len(), 1);
22440 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22441 assert_eq!(
22442 highlighted_edits.highlights[0].1.background_color,
22443 Some(cx.theme().status().created_background)
22444 );
22445 },
22446 )
22447 .await;
22448
22449 // Replacement
22450 assert_highlighted_edits(
22451 "This is a test.",
22452 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22453 false,
22454 cx,
22455 |highlighted_edits, cx| {
22456 assert_eq!(highlighted_edits.text, "That is a test.");
22457 assert_eq!(highlighted_edits.highlights.len(), 1);
22458 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22459 assert_eq!(
22460 highlighted_edits.highlights[0].1.background_color,
22461 Some(cx.theme().status().created_background)
22462 );
22463 },
22464 )
22465 .await;
22466
22467 // Multiple edits
22468 assert_highlighted_edits(
22469 "Hello, world!",
22470 vec![
22471 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22472 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22473 ],
22474 false,
22475 cx,
22476 |highlighted_edits, cx| {
22477 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22478 assert_eq!(highlighted_edits.highlights.len(), 2);
22479 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22480 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22481 assert_eq!(
22482 highlighted_edits.highlights[0].1.background_color,
22483 Some(cx.theme().status().created_background)
22484 );
22485 assert_eq!(
22486 highlighted_edits.highlights[1].1.background_color,
22487 Some(cx.theme().status().created_background)
22488 );
22489 },
22490 )
22491 .await;
22492
22493 // Multiple lines with edits
22494 assert_highlighted_edits(
22495 "First line\nSecond line\nThird line\nFourth line",
22496 vec![
22497 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22498 (
22499 Point::new(2, 0)..Point::new(2, 10),
22500 "New third line".to_string(),
22501 ),
22502 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22503 ],
22504 false,
22505 cx,
22506 |highlighted_edits, cx| {
22507 assert_eq!(
22508 highlighted_edits.text,
22509 "Second modified\nNew third line\nFourth updated line"
22510 );
22511 assert_eq!(highlighted_edits.highlights.len(), 3);
22512 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22513 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22514 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22515 for highlight in &highlighted_edits.highlights {
22516 assert_eq!(
22517 highlight.1.background_color,
22518 Some(cx.theme().status().created_background)
22519 );
22520 }
22521 },
22522 )
22523 .await;
22524}
22525
22526#[gpui::test]
22527async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22528 init_test(cx, |_| {});
22529
22530 // Deletion
22531 assert_highlighted_edits(
22532 "Hello, world!",
22533 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22534 true,
22535 cx,
22536 |highlighted_edits, cx| {
22537 assert_eq!(highlighted_edits.text, "Hello, world!");
22538 assert_eq!(highlighted_edits.highlights.len(), 1);
22539 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22540 assert_eq!(
22541 highlighted_edits.highlights[0].1.background_color,
22542 Some(cx.theme().status().deleted_background)
22543 );
22544 },
22545 )
22546 .await;
22547
22548 // Insertion
22549 assert_highlighted_edits(
22550 "Hello, world!",
22551 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22552 true,
22553 cx,
22554 |highlighted_edits, cx| {
22555 assert_eq!(highlighted_edits.highlights.len(), 1);
22556 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22557 assert_eq!(
22558 highlighted_edits.highlights[0].1.background_color,
22559 Some(cx.theme().status().created_background)
22560 );
22561 },
22562 )
22563 .await;
22564}
22565
22566async fn assert_highlighted_edits(
22567 text: &str,
22568 edits: Vec<(Range<Point>, String)>,
22569 include_deletions: bool,
22570 cx: &mut TestAppContext,
22571 assertion_fn: impl Fn(HighlightedText, &App),
22572) {
22573 let window = cx.add_window(|window, cx| {
22574 let buffer = MultiBuffer::build_simple(text, cx);
22575 Editor::new(EditorMode::full(), buffer, None, window, cx)
22576 });
22577 let cx = &mut VisualTestContext::from_window(*window, cx);
22578
22579 let (buffer, snapshot) = window
22580 .update(cx, |editor, _window, cx| {
22581 (
22582 editor.buffer().clone(),
22583 editor.buffer().read(cx).snapshot(cx),
22584 )
22585 })
22586 .unwrap();
22587
22588 let edits = edits
22589 .into_iter()
22590 .map(|(range, edit)| {
22591 (
22592 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22593 edit,
22594 )
22595 })
22596 .collect::<Vec<_>>();
22597
22598 let text_anchor_edits = edits
22599 .clone()
22600 .into_iter()
22601 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22602 .collect::<Vec<_>>();
22603
22604 let edit_preview = window
22605 .update(cx, |_, _window, cx| {
22606 buffer
22607 .read(cx)
22608 .as_singleton()
22609 .unwrap()
22610 .read(cx)
22611 .preview_edits(text_anchor_edits.into(), cx)
22612 })
22613 .unwrap()
22614 .await;
22615
22616 cx.update(|_window, cx| {
22617 let highlighted_edits = edit_prediction_edit_text(
22618 snapshot.as_singleton().unwrap().2,
22619 &edits,
22620 &edit_preview,
22621 include_deletions,
22622 cx,
22623 );
22624 assertion_fn(highlighted_edits, cx)
22625 });
22626}
22627
22628#[track_caller]
22629fn assert_breakpoint(
22630 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22631 path: &Arc<Path>,
22632 expected: Vec<(u32, Breakpoint)>,
22633) {
22634 if expected.is_empty() {
22635 assert!(!breakpoints.contains_key(path), "{}", path.display());
22636 } else {
22637 let mut breakpoint = breakpoints
22638 .get(path)
22639 .unwrap()
22640 .iter()
22641 .map(|breakpoint| {
22642 (
22643 breakpoint.row,
22644 Breakpoint {
22645 message: breakpoint.message.clone(),
22646 state: breakpoint.state,
22647 condition: breakpoint.condition.clone(),
22648 hit_condition: breakpoint.hit_condition.clone(),
22649 },
22650 )
22651 })
22652 .collect::<Vec<_>>();
22653
22654 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22655
22656 assert_eq!(expected, breakpoint);
22657 }
22658}
22659
22660fn add_log_breakpoint_at_cursor(
22661 editor: &mut Editor,
22662 log_message: &str,
22663 window: &mut Window,
22664 cx: &mut Context<Editor>,
22665) {
22666 let (anchor, bp) = editor
22667 .breakpoints_at_cursors(window, cx)
22668 .first()
22669 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22670 .unwrap_or_else(|| {
22671 let cursor_position: Point = editor.selections.newest(cx).head();
22672
22673 let breakpoint_position = editor
22674 .snapshot(window, cx)
22675 .display_snapshot
22676 .buffer_snapshot()
22677 .anchor_before(Point::new(cursor_position.row, 0));
22678
22679 (breakpoint_position, Breakpoint::new_log(log_message))
22680 });
22681
22682 editor.edit_breakpoint_at_anchor(
22683 anchor,
22684 bp,
22685 BreakpointEditAction::EditLogMessage(log_message.into()),
22686 cx,
22687 );
22688}
22689
22690#[gpui::test]
22691async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22692 init_test(cx, |_| {});
22693
22694 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22695 let fs = FakeFs::new(cx.executor());
22696 fs.insert_tree(
22697 path!("/a"),
22698 json!({
22699 "main.rs": sample_text,
22700 }),
22701 )
22702 .await;
22703 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22704 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22705 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22706
22707 let fs = FakeFs::new(cx.executor());
22708 fs.insert_tree(
22709 path!("/a"),
22710 json!({
22711 "main.rs": sample_text,
22712 }),
22713 )
22714 .await;
22715 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22716 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22717 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22718 let worktree_id = workspace
22719 .update(cx, |workspace, _window, cx| {
22720 workspace.project().update(cx, |project, cx| {
22721 project.worktrees(cx).next().unwrap().read(cx).id()
22722 })
22723 })
22724 .unwrap();
22725
22726 let buffer = project
22727 .update(cx, |project, cx| {
22728 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22729 })
22730 .await
22731 .unwrap();
22732
22733 let (editor, cx) = cx.add_window_view(|window, cx| {
22734 Editor::new(
22735 EditorMode::full(),
22736 MultiBuffer::build_from_buffer(buffer, cx),
22737 Some(project.clone()),
22738 window,
22739 cx,
22740 )
22741 });
22742
22743 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22744 let abs_path = project.read_with(cx, |project, cx| {
22745 project
22746 .absolute_path(&project_path, cx)
22747 .map(Arc::from)
22748 .unwrap()
22749 });
22750
22751 // assert we can add breakpoint on the first line
22752 editor.update_in(cx, |editor, window, cx| {
22753 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22754 editor.move_to_end(&MoveToEnd, window, cx);
22755 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22756 });
22757
22758 let breakpoints = editor.update(cx, |editor, cx| {
22759 editor
22760 .breakpoint_store()
22761 .as_ref()
22762 .unwrap()
22763 .read(cx)
22764 .all_source_breakpoints(cx)
22765 });
22766
22767 assert_eq!(1, breakpoints.len());
22768 assert_breakpoint(
22769 &breakpoints,
22770 &abs_path,
22771 vec![
22772 (0, Breakpoint::new_standard()),
22773 (3, Breakpoint::new_standard()),
22774 ],
22775 );
22776
22777 editor.update_in(cx, |editor, window, cx| {
22778 editor.move_to_beginning(&MoveToBeginning, window, cx);
22779 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22780 });
22781
22782 let breakpoints = editor.update(cx, |editor, cx| {
22783 editor
22784 .breakpoint_store()
22785 .as_ref()
22786 .unwrap()
22787 .read(cx)
22788 .all_source_breakpoints(cx)
22789 });
22790
22791 assert_eq!(1, breakpoints.len());
22792 assert_breakpoint(
22793 &breakpoints,
22794 &abs_path,
22795 vec![(3, Breakpoint::new_standard())],
22796 );
22797
22798 editor.update_in(cx, |editor, window, cx| {
22799 editor.move_to_end(&MoveToEnd, window, cx);
22800 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22801 });
22802
22803 let breakpoints = editor.update(cx, |editor, cx| {
22804 editor
22805 .breakpoint_store()
22806 .as_ref()
22807 .unwrap()
22808 .read(cx)
22809 .all_source_breakpoints(cx)
22810 });
22811
22812 assert_eq!(0, breakpoints.len());
22813 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22814}
22815
22816#[gpui::test]
22817async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22818 init_test(cx, |_| {});
22819
22820 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22821
22822 let fs = FakeFs::new(cx.executor());
22823 fs.insert_tree(
22824 path!("/a"),
22825 json!({
22826 "main.rs": sample_text,
22827 }),
22828 )
22829 .await;
22830 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22831 let (workspace, cx) =
22832 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22833
22834 let worktree_id = workspace.update(cx, |workspace, cx| {
22835 workspace.project().update(cx, |project, cx| {
22836 project.worktrees(cx).next().unwrap().read(cx).id()
22837 })
22838 });
22839
22840 let buffer = project
22841 .update(cx, |project, cx| {
22842 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22843 })
22844 .await
22845 .unwrap();
22846
22847 let (editor, cx) = cx.add_window_view(|window, cx| {
22848 Editor::new(
22849 EditorMode::full(),
22850 MultiBuffer::build_from_buffer(buffer, cx),
22851 Some(project.clone()),
22852 window,
22853 cx,
22854 )
22855 });
22856
22857 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22858 let abs_path = project.read_with(cx, |project, cx| {
22859 project
22860 .absolute_path(&project_path, cx)
22861 .map(Arc::from)
22862 .unwrap()
22863 });
22864
22865 editor.update_in(cx, |editor, window, cx| {
22866 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22867 });
22868
22869 let breakpoints = editor.update(cx, |editor, cx| {
22870 editor
22871 .breakpoint_store()
22872 .as_ref()
22873 .unwrap()
22874 .read(cx)
22875 .all_source_breakpoints(cx)
22876 });
22877
22878 assert_breakpoint(
22879 &breakpoints,
22880 &abs_path,
22881 vec![(0, Breakpoint::new_log("hello world"))],
22882 );
22883
22884 // Removing a log message from a log breakpoint should remove it
22885 editor.update_in(cx, |editor, window, cx| {
22886 add_log_breakpoint_at_cursor(editor, "", window, cx);
22887 });
22888
22889 let breakpoints = editor.update(cx, |editor, cx| {
22890 editor
22891 .breakpoint_store()
22892 .as_ref()
22893 .unwrap()
22894 .read(cx)
22895 .all_source_breakpoints(cx)
22896 });
22897
22898 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22899
22900 editor.update_in(cx, |editor, window, cx| {
22901 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22902 editor.move_to_end(&MoveToEnd, window, cx);
22903 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22904 // Not adding a log message to a standard breakpoint shouldn't remove it
22905 add_log_breakpoint_at_cursor(editor, "", window, cx);
22906 });
22907
22908 let breakpoints = editor.update(cx, |editor, cx| {
22909 editor
22910 .breakpoint_store()
22911 .as_ref()
22912 .unwrap()
22913 .read(cx)
22914 .all_source_breakpoints(cx)
22915 });
22916
22917 assert_breakpoint(
22918 &breakpoints,
22919 &abs_path,
22920 vec![
22921 (0, Breakpoint::new_standard()),
22922 (3, Breakpoint::new_standard()),
22923 ],
22924 );
22925
22926 editor.update_in(cx, |editor, window, cx| {
22927 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22928 });
22929
22930 let breakpoints = editor.update(cx, |editor, cx| {
22931 editor
22932 .breakpoint_store()
22933 .as_ref()
22934 .unwrap()
22935 .read(cx)
22936 .all_source_breakpoints(cx)
22937 });
22938
22939 assert_breakpoint(
22940 &breakpoints,
22941 &abs_path,
22942 vec![
22943 (0, Breakpoint::new_standard()),
22944 (3, Breakpoint::new_log("hello world")),
22945 ],
22946 );
22947
22948 editor.update_in(cx, |editor, window, cx| {
22949 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22950 });
22951
22952 let breakpoints = editor.update(cx, |editor, cx| {
22953 editor
22954 .breakpoint_store()
22955 .as_ref()
22956 .unwrap()
22957 .read(cx)
22958 .all_source_breakpoints(cx)
22959 });
22960
22961 assert_breakpoint(
22962 &breakpoints,
22963 &abs_path,
22964 vec![
22965 (0, Breakpoint::new_standard()),
22966 (3, Breakpoint::new_log("hello Earth!!")),
22967 ],
22968 );
22969}
22970
22971/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22972/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22973/// or when breakpoints were placed out of order. This tests for a regression too
22974#[gpui::test]
22975async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22976 init_test(cx, |_| {});
22977
22978 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22979 let fs = FakeFs::new(cx.executor());
22980 fs.insert_tree(
22981 path!("/a"),
22982 json!({
22983 "main.rs": sample_text,
22984 }),
22985 )
22986 .await;
22987 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22988 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22989 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22990
22991 let fs = FakeFs::new(cx.executor());
22992 fs.insert_tree(
22993 path!("/a"),
22994 json!({
22995 "main.rs": sample_text,
22996 }),
22997 )
22998 .await;
22999 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23000 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23001 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23002 let worktree_id = workspace
23003 .update(cx, |workspace, _window, cx| {
23004 workspace.project().update(cx, |project, cx| {
23005 project.worktrees(cx).next().unwrap().read(cx).id()
23006 })
23007 })
23008 .unwrap();
23009
23010 let buffer = project
23011 .update(cx, |project, cx| {
23012 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23013 })
23014 .await
23015 .unwrap();
23016
23017 let (editor, cx) = cx.add_window_view(|window, cx| {
23018 Editor::new(
23019 EditorMode::full(),
23020 MultiBuffer::build_from_buffer(buffer, cx),
23021 Some(project.clone()),
23022 window,
23023 cx,
23024 )
23025 });
23026
23027 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23028 let abs_path = project.read_with(cx, |project, cx| {
23029 project
23030 .absolute_path(&project_path, cx)
23031 .map(Arc::from)
23032 .unwrap()
23033 });
23034
23035 // assert we can add breakpoint on the first line
23036 editor.update_in(cx, |editor, window, cx| {
23037 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23038 editor.move_to_end(&MoveToEnd, window, cx);
23039 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23040 editor.move_up(&MoveUp, window, cx);
23041 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23042 });
23043
23044 let breakpoints = editor.update(cx, |editor, cx| {
23045 editor
23046 .breakpoint_store()
23047 .as_ref()
23048 .unwrap()
23049 .read(cx)
23050 .all_source_breakpoints(cx)
23051 });
23052
23053 assert_eq!(1, breakpoints.len());
23054 assert_breakpoint(
23055 &breakpoints,
23056 &abs_path,
23057 vec![
23058 (0, Breakpoint::new_standard()),
23059 (2, Breakpoint::new_standard()),
23060 (3, Breakpoint::new_standard()),
23061 ],
23062 );
23063
23064 editor.update_in(cx, |editor, window, cx| {
23065 editor.move_to_beginning(&MoveToBeginning, window, cx);
23066 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23067 editor.move_to_end(&MoveToEnd, window, cx);
23068 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23069 // Disabling a breakpoint that doesn't exist should do nothing
23070 editor.move_up(&MoveUp, window, cx);
23071 editor.move_up(&MoveUp, window, cx);
23072 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23073 });
23074
23075 let breakpoints = editor.update(cx, |editor, cx| {
23076 editor
23077 .breakpoint_store()
23078 .as_ref()
23079 .unwrap()
23080 .read(cx)
23081 .all_source_breakpoints(cx)
23082 });
23083
23084 let disable_breakpoint = {
23085 let mut bp = Breakpoint::new_standard();
23086 bp.state = BreakpointState::Disabled;
23087 bp
23088 };
23089
23090 assert_eq!(1, breakpoints.len());
23091 assert_breakpoint(
23092 &breakpoints,
23093 &abs_path,
23094 vec![
23095 (0, disable_breakpoint.clone()),
23096 (2, Breakpoint::new_standard()),
23097 (3, disable_breakpoint.clone()),
23098 ],
23099 );
23100
23101 editor.update_in(cx, |editor, window, cx| {
23102 editor.move_to_beginning(&MoveToBeginning, window, cx);
23103 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23104 editor.move_to_end(&MoveToEnd, window, cx);
23105 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23106 editor.move_up(&MoveUp, window, cx);
23107 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23108 });
23109
23110 let breakpoints = editor.update(cx, |editor, cx| {
23111 editor
23112 .breakpoint_store()
23113 .as_ref()
23114 .unwrap()
23115 .read(cx)
23116 .all_source_breakpoints(cx)
23117 });
23118
23119 assert_eq!(1, breakpoints.len());
23120 assert_breakpoint(
23121 &breakpoints,
23122 &abs_path,
23123 vec![
23124 (0, Breakpoint::new_standard()),
23125 (2, disable_breakpoint),
23126 (3, Breakpoint::new_standard()),
23127 ],
23128 );
23129}
23130
23131#[gpui::test]
23132async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23133 init_test(cx, |_| {});
23134 let capabilities = lsp::ServerCapabilities {
23135 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23136 prepare_provider: Some(true),
23137 work_done_progress_options: Default::default(),
23138 })),
23139 ..Default::default()
23140 };
23141 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23142
23143 cx.set_state(indoc! {"
23144 struct Fˇoo {}
23145 "});
23146
23147 cx.update_editor(|editor, _, cx| {
23148 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23149 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23150 editor.highlight_background::<DocumentHighlightRead>(
23151 &[highlight_range],
23152 |theme| theme.colors().editor_document_highlight_read_background,
23153 cx,
23154 );
23155 });
23156
23157 let mut prepare_rename_handler = cx
23158 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23159 move |_, _, _| async move {
23160 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23161 start: lsp::Position {
23162 line: 0,
23163 character: 7,
23164 },
23165 end: lsp::Position {
23166 line: 0,
23167 character: 10,
23168 },
23169 })))
23170 },
23171 );
23172 let prepare_rename_task = cx
23173 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23174 .expect("Prepare rename was not started");
23175 prepare_rename_handler.next().await.unwrap();
23176 prepare_rename_task.await.expect("Prepare rename failed");
23177
23178 let mut rename_handler =
23179 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23180 let edit = lsp::TextEdit {
23181 range: lsp::Range {
23182 start: lsp::Position {
23183 line: 0,
23184 character: 7,
23185 },
23186 end: lsp::Position {
23187 line: 0,
23188 character: 10,
23189 },
23190 },
23191 new_text: "FooRenamed".to_string(),
23192 };
23193 Ok(Some(lsp::WorkspaceEdit::new(
23194 // Specify the same edit twice
23195 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23196 )))
23197 });
23198 let rename_task = cx
23199 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23200 .expect("Confirm rename was not started");
23201 rename_handler.next().await.unwrap();
23202 rename_task.await.expect("Confirm rename failed");
23203 cx.run_until_parked();
23204
23205 // Despite two edits, only one is actually applied as those are identical
23206 cx.assert_editor_state(indoc! {"
23207 struct FooRenamedˇ {}
23208 "});
23209}
23210
23211#[gpui::test]
23212async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23213 init_test(cx, |_| {});
23214 // These capabilities indicate that the server does not support prepare rename.
23215 let capabilities = lsp::ServerCapabilities {
23216 rename_provider: Some(lsp::OneOf::Left(true)),
23217 ..Default::default()
23218 };
23219 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23220
23221 cx.set_state(indoc! {"
23222 struct Fˇoo {}
23223 "});
23224
23225 cx.update_editor(|editor, _window, cx| {
23226 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23227 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23228 editor.highlight_background::<DocumentHighlightRead>(
23229 &[highlight_range],
23230 |theme| theme.colors().editor_document_highlight_read_background,
23231 cx,
23232 );
23233 });
23234
23235 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23236 .expect("Prepare rename was not started")
23237 .await
23238 .expect("Prepare rename failed");
23239
23240 let mut rename_handler =
23241 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23242 let edit = lsp::TextEdit {
23243 range: lsp::Range {
23244 start: lsp::Position {
23245 line: 0,
23246 character: 7,
23247 },
23248 end: lsp::Position {
23249 line: 0,
23250 character: 10,
23251 },
23252 },
23253 new_text: "FooRenamed".to_string(),
23254 };
23255 Ok(Some(lsp::WorkspaceEdit::new(
23256 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23257 )))
23258 });
23259 let rename_task = cx
23260 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23261 .expect("Confirm rename was not started");
23262 rename_handler.next().await.unwrap();
23263 rename_task.await.expect("Confirm rename failed");
23264 cx.run_until_parked();
23265
23266 // Correct range is renamed, as `surrounding_word` is used to find it.
23267 cx.assert_editor_state(indoc! {"
23268 struct FooRenamedˇ {}
23269 "});
23270}
23271
23272#[gpui::test]
23273async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23274 init_test(cx, |_| {});
23275 let mut cx = EditorTestContext::new(cx).await;
23276
23277 let language = Arc::new(
23278 Language::new(
23279 LanguageConfig::default(),
23280 Some(tree_sitter_html::LANGUAGE.into()),
23281 )
23282 .with_brackets_query(
23283 r#"
23284 ("<" @open "/>" @close)
23285 ("</" @open ">" @close)
23286 ("<" @open ">" @close)
23287 ("\"" @open "\"" @close)
23288 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23289 "#,
23290 )
23291 .unwrap(),
23292 );
23293 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23294
23295 cx.set_state(indoc! {"
23296 <span>ˇ</span>
23297 "});
23298 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23299 cx.assert_editor_state(indoc! {"
23300 <span>
23301 ˇ
23302 </span>
23303 "});
23304
23305 cx.set_state(indoc! {"
23306 <span><span></span>ˇ</span>
23307 "});
23308 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23309 cx.assert_editor_state(indoc! {"
23310 <span><span></span>
23311 ˇ</span>
23312 "});
23313
23314 cx.set_state(indoc! {"
23315 <span>ˇ
23316 </span>
23317 "});
23318 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23319 cx.assert_editor_state(indoc! {"
23320 <span>
23321 ˇ
23322 </span>
23323 "});
23324}
23325
23326#[gpui::test(iterations = 10)]
23327async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23328 init_test(cx, |_| {});
23329
23330 let fs = FakeFs::new(cx.executor());
23331 fs.insert_tree(
23332 path!("/dir"),
23333 json!({
23334 "a.ts": "a",
23335 }),
23336 )
23337 .await;
23338
23339 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23340 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23341 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23342
23343 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23344 language_registry.add(Arc::new(Language::new(
23345 LanguageConfig {
23346 name: "TypeScript".into(),
23347 matcher: LanguageMatcher {
23348 path_suffixes: vec!["ts".to_string()],
23349 ..Default::default()
23350 },
23351 ..Default::default()
23352 },
23353 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23354 )));
23355 let mut fake_language_servers = language_registry.register_fake_lsp(
23356 "TypeScript",
23357 FakeLspAdapter {
23358 capabilities: lsp::ServerCapabilities {
23359 code_lens_provider: Some(lsp::CodeLensOptions {
23360 resolve_provider: Some(true),
23361 }),
23362 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23363 commands: vec!["_the/command".to_string()],
23364 ..lsp::ExecuteCommandOptions::default()
23365 }),
23366 ..lsp::ServerCapabilities::default()
23367 },
23368 ..FakeLspAdapter::default()
23369 },
23370 );
23371
23372 let editor = workspace
23373 .update(cx, |workspace, window, cx| {
23374 workspace.open_abs_path(
23375 PathBuf::from(path!("/dir/a.ts")),
23376 OpenOptions::default(),
23377 window,
23378 cx,
23379 )
23380 })
23381 .unwrap()
23382 .await
23383 .unwrap()
23384 .downcast::<Editor>()
23385 .unwrap();
23386 cx.executor().run_until_parked();
23387
23388 let fake_server = fake_language_servers.next().await.unwrap();
23389
23390 let buffer = editor.update(cx, |editor, cx| {
23391 editor
23392 .buffer()
23393 .read(cx)
23394 .as_singleton()
23395 .expect("have opened a single file by path")
23396 });
23397
23398 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23399 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23400 drop(buffer_snapshot);
23401 let actions = cx
23402 .update_window(*workspace, |_, window, cx| {
23403 project.code_actions(&buffer, anchor..anchor, window, cx)
23404 })
23405 .unwrap();
23406
23407 fake_server
23408 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23409 Ok(Some(vec![
23410 lsp::CodeLens {
23411 range: lsp::Range::default(),
23412 command: Some(lsp::Command {
23413 title: "Code lens command".to_owned(),
23414 command: "_the/command".to_owned(),
23415 arguments: None,
23416 }),
23417 data: None,
23418 },
23419 lsp::CodeLens {
23420 range: lsp::Range::default(),
23421 command: Some(lsp::Command {
23422 title: "Command not in capabilities".to_owned(),
23423 command: "not in capabilities".to_owned(),
23424 arguments: None,
23425 }),
23426 data: None,
23427 },
23428 lsp::CodeLens {
23429 range: lsp::Range {
23430 start: lsp::Position {
23431 line: 1,
23432 character: 1,
23433 },
23434 end: lsp::Position {
23435 line: 1,
23436 character: 1,
23437 },
23438 },
23439 command: Some(lsp::Command {
23440 title: "Command not in range".to_owned(),
23441 command: "_the/command".to_owned(),
23442 arguments: None,
23443 }),
23444 data: None,
23445 },
23446 ]))
23447 })
23448 .next()
23449 .await;
23450
23451 let actions = actions.await.unwrap();
23452 assert_eq!(
23453 actions.len(),
23454 1,
23455 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23456 );
23457 let action = actions[0].clone();
23458 let apply = project.update(cx, |project, cx| {
23459 project.apply_code_action(buffer.clone(), action, true, cx)
23460 });
23461
23462 // Resolving the code action does not populate its edits. In absence of
23463 // edits, we must execute the given command.
23464 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23465 |mut lens, _| async move {
23466 let lens_command = lens.command.as_mut().expect("should have a command");
23467 assert_eq!(lens_command.title, "Code lens command");
23468 lens_command.arguments = Some(vec![json!("the-argument")]);
23469 Ok(lens)
23470 },
23471 );
23472
23473 // While executing the command, the language server sends the editor
23474 // a `workspaceEdit` request.
23475 fake_server
23476 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23477 let fake = fake_server.clone();
23478 move |params, _| {
23479 assert_eq!(params.command, "_the/command");
23480 let fake = fake.clone();
23481 async move {
23482 fake.server
23483 .request::<lsp::request::ApplyWorkspaceEdit>(
23484 lsp::ApplyWorkspaceEditParams {
23485 label: None,
23486 edit: lsp::WorkspaceEdit {
23487 changes: Some(
23488 [(
23489 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23490 vec![lsp::TextEdit {
23491 range: lsp::Range::new(
23492 lsp::Position::new(0, 0),
23493 lsp::Position::new(0, 0),
23494 ),
23495 new_text: "X".into(),
23496 }],
23497 )]
23498 .into_iter()
23499 .collect(),
23500 ),
23501 ..lsp::WorkspaceEdit::default()
23502 },
23503 },
23504 )
23505 .await
23506 .into_response()
23507 .unwrap();
23508 Ok(Some(json!(null)))
23509 }
23510 }
23511 })
23512 .next()
23513 .await;
23514
23515 // Applying the code lens command returns a project transaction containing the edits
23516 // sent by the language server in its `workspaceEdit` request.
23517 let transaction = apply.await.unwrap();
23518 assert!(transaction.0.contains_key(&buffer));
23519 buffer.update(cx, |buffer, cx| {
23520 assert_eq!(buffer.text(), "Xa");
23521 buffer.undo(cx);
23522 assert_eq!(buffer.text(), "a");
23523 });
23524
23525 let actions_after_edits = cx
23526 .update_window(*workspace, |_, window, cx| {
23527 project.code_actions(&buffer, anchor..anchor, window, cx)
23528 })
23529 .unwrap()
23530 .await
23531 .unwrap();
23532 assert_eq!(
23533 actions, actions_after_edits,
23534 "For the same selection, same code lens actions should be returned"
23535 );
23536
23537 let _responses =
23538 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23539 panic!("No more code lens requests are expected");
23540 });
23541 editor.update_in(cx, |editor, window, cx| {
23542 editor.select_all(&SelectAll, window, cx);
23543 });
23544 cx.executor().run_until_parked();
23545 let new_actions = cx
23546 .update_window(*workspace, |_, window, cx| {
23547 project.code_actions(&buffer, anchor..anchor, window, cx)
23548 })
23549 .unwrap()
23550 .await
23551 .unwrap();
23552 assert_eq!(
23553 actions, new_actions,
23554 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23555 );
23556}
23557
23558#[gpui::test]
23559async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23560 init_test(cx, |_| {});
23561
23562 let fs = FakeFs::new(cx.executor());
23563 let main_text = r#"fn main() {
23564println!("1");
23565println!("2");
23566println!("3");
23567println!("4");
23568println!("5");
23569}"#;
23570 let lib_text = "mod foo {}";
23571 fs.insert_tree(
23572 path!("/a"),
23573 json!({
23574 "lib.rs": lib_text,
23575 "main.rs": main_text,
23576 }),
23577 )
23578 .await;
23579
23580 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23581 let (workspace, cx) =
23582 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23583 let worktree_id = workspace.update(cx, |workspace, cx| {
23584 workspace.project().update(cx, |project, cx| {
23585 project.worktrees(cx).next().unwrap().read(cx).id()
23586 })
23587 });
23588
23589 let expected_ranges = vec![
23590 Point::new(0, 0)..Point::new(0, 0),
23591 Point::new(1, 0)..Point::new(1, 1),
23592 Point::new(2, 0)..Point::new(2, 2),
23593 Point::new(3, 0)..Point::new(3, 3),
23594 ];
23595
23596 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23597 let editor_1 = workspace
23598 .update_in(cx, |workspace, window, cx| {
23599 workspace.open_path(
23600 (worktree_id, rel_path("main.rs")),
23601 Some(pane_1.downgrade()),
23602 true,
23603 window,
23604 cx,
23605 )
23606 })
23607 .unwrap()
23608 .await
23609 .downcast::<Editor>()
23610 .unwrap();
23611 pane_1.update(cx, |pane, cx| {
23612 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23613 open_editor.update(cx, |editor, cx| {
23614 assert_eq!(
23615 editor.display_text(cx),
23616 main_text,
23617 "Original main.rs text on initial open",
23618 );
23619 assert_eq!(
23620 editor
23621 .selections
23622 .all::<Point>(cx)
23623 .into_iter()
23624 .map(|s| s.range())
23625 .collect::<Vec<_>>(),
23626 vec![Point::zero()..Point::zero()],
23627 "Default selections on initial open",
23628 );
23629 })
23630 });
23631 editor_1.update_in(cx, |editor, window, cx| {
23632 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23633 s.select_ranges(expected_ranges.clone());
23634 });
23635 });
23636
23637 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23638 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23639 });
23640 let editor_2 = workspace
23641 .update_in(cx, |workspace, window, cx| {
23642 workspace.open_path(
23643 (worktree_id, rel_path("main.rs")),
23644 Some(pane_2.downgrade()),
23645 true,
23646 window,
23647 cx,
23648 )
23649 })
23650 .unwrap()
23651 .await
23652 .downcast::<Editor>()
23653 .unwrap();
23654 pane_2.update(cx, |pane, cx| {
23655 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23656 open_editor.update(cx, |editor, cx| {
23657 assert_eq!(
23658 editor.display_text(cx),
23659 main_text,
23660 "Original main.rs text on initial open in another panel",
23661 );
23662 assert_eq!(
23663 editor
23664 .selections
23665 .all::<Point>(cx)
23666 .into_iter()
23667 .map(|s| s.range())
23668 .collect::<Vec<_>>(),
23669 vec![Point::zero()..Point::zero()],
23670 "Default selections on initial open in another panel",
23671 );
23672 })
23673 });
23674
23675 editor_2.update_in(cx, |editor, window, cx| {
23676 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23677 });
23678
23679 let _other_editor_1 = workspace
23680 .update_in(cx, |workspace, window, cx| {
23681 workspace.open_path(
23682 (worktree_id, rel_path("lib.rs")),
23683 Some(pane_1.downgrade()),
23684 true,
23685 window,
23686 cx,
23687 )
23688 })
23689 .unwrap()
23690 .await
23691 .downcast::<Editor>()
23692 .unwrap();
23693 pane_1
23694 .update_in(cx, |pane, window, cx| {
23695 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23696 })
23697 .await
23698 .unwrap();
23699 drop(editor_1);
23700 pane_1.update(cx, |pane, cx| {
23701 pane.active_item()
23702 .unwrap()
23703 .downcast::<Editor>()
23704 .unwrap()
23705 .update(cx, |editor, cx| {
23706 assert_eq!(
23707 editor.display_text(cx),
23708 lib_text,
23709 "Other file should be open and active",
23710 );
23711 });
23712 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23713 });
23714
23715 let _other_editor_2 = workspace
23716 .update_in(cx, |workspace, window, cx| {
23717 workspace.open_path(
23718 (worktree_id, rel_path("lib.rs")),
23719 Some(pane_2.downgrade()),
23720 true,
23721 window,
23722 cx,
23723 )
23724 })
23725 .unwrap()
23726 .await
23727 .downcast::<Editor>()
23728 .unwrap();
23729 pane_2
23730 .update_in(cx, |pane, window, cx| {
23731 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23732 })
23733 .await
23734 .unwrap();
23735 drop(editor_2);
23736 pane_2.update(cx, |pane, cx| {
23737 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23738 open_editor.update(cx, |editor, cx| {
23739 assert_eq!(
23740 editor.display_text(cx),
23741 lib_text,
23742 "Other file should be open and active in another panel too",
23743 );
23744 });
23745 assert_eq!(
23746 pane.items().count(),
23747 1,
23748 "No other editors should be open in another pane",
23749 );
23750 });
23751
23752 let _editor_1_reopened = workspace
23753 .update_in(cx, |workspace, window, cx| {
23754 workspace.open_path(
23755 (worktree_id, rel_path("main.rs")),
23756 Some(pane_1.downgrade()),
23757 true,
23758 window,
23759 cx,
23760 )
23761 })
23762 .unwrap()
23763 .await
23764 .downcast::<Editor>()
23765 .unwrap();
23766 let _editor_2_reopened = workspace
23767 .update_in(cx, |workspace, window, cx| {
23768 workspace.open_path(
23769 (worktree_id, rel_path("main.rs")),
23770 Some(pane_2.downgrade()),
23771 true,
23772 window,
23773 cx,
23774 )
23775 })
23776 .unwrap()
23777 .await
23778 .downcast::<Editor>()
23779 .unwrap();
23780 pane_1.update(cx, |pane, cx| {
23781 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23782 open_editor.update(cx, |editor, cx| {
23783 assert_eq!(
23784 editor.display_text(cx),
23785 main_text,
23786 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23787 );
23788 assert_eq!(
23789 editor
23790 .selections
23791 .all::<Point>(cx)
23792 .into_iter()
23793 .map(|s| s.range())
23794 .collect::<Vec<_>>(),
23795 expected_ranges,
23796 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23797 );
23798 })
23799 });
23800 pane_2.update(cx, |pane, cx| {
23801 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23802 open_editor.update(cx, |editor, cx| {
23803 assert_eq!(
23804 editor.display_text(cx),
23805 r#"fn main() {
23806⋯rintln!("1");
23807⋯intln!("2");
23808⋯ntln!("3");
23809println!("4");
23810println!("5");
23811}"#,
23812 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23813 );
23814 assert_eq!(
23815 editor
23816 .selections
23817 .all::<Point>(cx)
23818 .into_iter()
23819 .map(|s| s.range())
23820 .collect::<Vec<_>>(),
23821 vec![Point::zero()..Point::zero()],
23822 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23823 );
23824 })
23825 });
23826}
23827
23828#[gpui::test]
23829async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23830 init_test(cx, |_| {});
23831
23832 let fs = FakeFs::new(cx.executor());
23833 let main_text = r#"fn main() {
23834println!("1");
23835println!("2");
23836println!("3");
23837println!("4");
23838println!("5");
23839}"#;
23840 let lib_text = "mod foo {}";
23841 fs.insert_tree(
23842 path!("/a"),
23843 json!({
23844 "lib.rs": lib_text,
23845 "main.rs": main_text,
23846 }),
23847 )
23848 .await;
23849
23850 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23851 let (workspace, cx) =
23852 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23853 let worktree_id = workspace.update(cx, |workspace, cx| {
23854 workspace.project().update(cx, |project, cx| {
23855 project.worktrees(cx).next().unwrap().read(cx).id()
23856 })
23857 });
23858
23859 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23860 let editor = workspace
23861 .update_in(cx, |workspace, window, cx| {
23862 workspace.open_path(
23863 (worktree_id, rel_path("main.rs")),
23864 Some(pane.downgrade()),
23865 true,
23866 window,
23867 cx,
23868 )
23869 })
23870 .unwrap()
23871 .await
23872 .downcast::<Editor>()
23873 .unwrap();
23874 pane.update(cx, |pane, cx| {
23875 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23876 open_editor.update(cx, |editor, cx| {
23877 assert_eq!(
23878 editor.display_text(cx),
23879 main_text,
23880 "Original main.rs text on initial open",
23881 );
23882 })
23883 });
23884 editor.update_in(cx, |editor, window, cx| {
23885 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23886 });
23887
23888 cx.update_global(|store: &mut SettingsStore, cx| {
23889 store.update_user_settings(cx, |s| {
23890 s.workspace.restore_on_file_reopen = Some(false);
23891 });
23892 });
23893 editor.update_in(cx, |editor, window, cx| {
23894 editor.fold_ranges(
23895 vec![
23896 Point::new(1, 0)..Point::new(1, 1),
23897 Point::new(2, 0)..Point::new(2, 2),
23898 Point::new(3, 0)..Point::new(3, 3),
23899 ],
23900 false,
23901 window,
23902 cx,
23903 );
23904 });
23905 pane.update_in(cx, |pane, window, cx| {
23906 pane.close_all_items(&CloseAllItems::default(), window, cx)
23907 })
23908 .await
23909 .unwrap();
23910 pane.update(cx, |pane, _| {
23911 assert!(pane.active_item().is_none());
23912 });
23913 cx.update_global(|store: &mut SettingsStore, cx| {
23914 store.update_user_settings(cx, |s| {
23915 s.workspace.restore_on_file_reopen = Some(true);
23916 });
23917 });
23918
23919 let _editor_reopened = workspace
23920 .update_in(cx, |workspace, window, cx| {
23921 workspace.open_path(
23922 (worktree_id, rel_path("main.rs")),
23923 Some(pane.downgrade()),
23924 true,
23925 window,
23926 cx,
23927 )
23928 })
23929 .unwrap()
23930 .await
23931 .downcast::<Editor>()
23932 .unwrap();
23933 pane.update(cx, |pane, cx| {
23934 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23935 open_editor.update(cx, |editor, cx| {
23936 assert_eq!(
23937 editor.display_text(cx),
23938 main_text,
23939 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23940 );
23941 })
23942 });
23943}
23944
23945#[gpui::test]
23946async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23947 struct EmptyModalView {
23948 focus_handle: gpui::FocusHandle,
23949 }
23950 impl EventEmitter<DismissEvent> for EmptyModalView {}
23951 impl Render for EmptyModalView {
23952 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23953 div()
23954 }
23955 }
23956 impl Focusable for EmptyModalView {
23957 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23958 self.focus_handle.clone()
23959 }
23960 }
23961 impl workspace::ModalView for EmptyModalView {}
23962 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23963 EmptyModalView {
23964 focus_handle: cx.focus_handle(),
23965 }
23966 }
23967
23968 init_test(cx, |_| {});
23969
23970 let fs = FakeFs::new(cx.executor());
23971 let project = Project::test(fs, [], cx).await;
23972 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23973 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23974 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23975 let editor = cx.new_window_entity(|window, cx| {
23976 Editor::new(
23977 EditorMode::full(),
23978 buffer,
23979 Some(project.clone()),
23980 window,
23981 cx,
23982 )
23983 });
23984 workspace
23985 .update(cx, |workspace, window, cx| {
23986 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23987 })
23988 .unwrap();
23989 editor.update_in(cx, |editor, window, cx| {
23990 editor.open_context_menu(&OpenContextMenu, window, cx);
23991 assert!(editor.mouse_context_menu.is_some());
23992 });
23993 workspace
23994 .update(cx, |workspace, window, cx| {
23995 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23996 })
23997 .unwrap();
23998 cx.read(|cx| {
23999 assert!(editor.read(cx).mouse_context_menu.is_none());
24000 });
24001}
24002
24003fn set_linked_edit_ranges(
24004 opening: (Point, Point),
24005 closing: (Point, Point),
24006 editor: &mut Editor,
24007 cx: &mut Context<Editor>,
24008) {
24009 let Some((buffer, _)) = editor
24010 .buffer
24011 .read(cx)
24012 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24013 else {
24014 panic!("Failed to get buffer for selection position");
24015 };
24016 let buffer = buffer.read(cx);
24017 let buffer_id = buffer.remote_id();
24018 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24019 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24020 let mut linked_ranges = HashMap::default();
24021 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24022 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24023}
24024
24025#[gpui::test]
24026async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24027 init_test(cx, |_| {});
24028
24029 let fs = FakeFs::new(cx.executor());
24030 fs.insert_file(path!("/file.html"), Default::default())
24031 .await;
24032
24033 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24034
24035 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24036 let html_language = Arc::new(Language::new(
24037 LanguageConfig {
24038 name: "HTML".into(),
24039 matcher: LanguageMatcher {
24040 path_suffixes: vec!["html".to_string()],
24041 ..LanguageMatcher::default()
24042 },
24043 brackets: BracketPairConfig {
24044 pairs: vec![BracketPair {
24045 start: "<".into(),
24046 end: ">".into(),
24047 close: true,
24048 ..Default::default()
24049 }],
24050 ..Default::default()
24051 },
24052 ..Default::default()
24053 },
24054 Some(tree_sitter_html::LANGUAGE.into()),
24055 ));
24056 language_registry.add(html_language);
24057 let mut fake_servers = language_registry.register_fake_lsp(
24058 "HTML",
24059 FakeLspAdapter {
24060 capabilities: lsp::ServerCapabilities {
24061 completion_provider: Some(lsp::CompletionOptions {
24062 resolve_provider: Some(true),
24063 ..Default::default()
24064 }),
24065 ..Default::default()
24066 },
24067 ..Default::default()
24068 },
24069 );
24070
24071 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24072 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24073
24074 let worktree_id = workspace
24075 .update(cx, |workspace, _window, cx| {
24076 workspace.project().update(cx, |project, cx| {
24077 project.worktrees(cx).next().unwrap().read(cx).id()
24078 })
24079 })
24080 .unwrap();
24081 project
24082 .update(cx, |project, cx| {
24083 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24084 })
24085 .await
24086 .unwrap();
24087 let editor = workspace
24088 .update(cx, |workspace, window, cx| {
24089 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24090 })
24091 .unwrap()
24092 .await
24093 .unwrap()
24094 .downcast::<Editor>()
24095 .unwrap();
24096
24097 let fake_server = fake_servers.next().await.unwrap();
24098 editor.update_in(cx, |editor, window, cx| {
24099 editor.set_text("<ad></ad>", window, cx);
24100 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24101 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24102 });
24103 set_linked_edit_ranges(
24104 (Point::new(0, 1), Point::new(0, 3)),
24105 (Point::new(0, 6), Point::new(0, 8)),
24106 editor,
24107 cx,
24108 );
24109 });
24110 let mut completion_handle =
24111 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24112 Ok(Some(lsp::CompletionResponse::Array(vec![
24113 lsp::CompletionItem {
24114 label: "head".to_string(),
24115 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24116 lsp::InsertReplaceEdit {
24117 new_text: "head".to_string(),
24118 insert: lsp::Range::new(
24119 lsp::Position::new(0, 1),
24120 lsp::Position::new(0, 3),
24121 ),
24122 replace: lsp::Range::new(
24123 lsp::Position::new(0, 1),
24124 lsp::Position::new(0, 3),
24125 ),
24126 },
24127 )),
24128 ..Default::default()
24129 },
24130 ])))
24131 });
24132 editor.update_in(cx, |editor, window, cx| {
24133 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
24134 });
24135 cx.run_until_parked();
24136 completion_handle.next().await.unwrap();
24137 editor.update(cx, |editor, _| {
24138 assert!(
24139 editor.context_menu_visible(),
24140 "Completion menu should be visible"
24141 );
24142 });
24143 editor.update_in(cx, |editor, window, cx| {
24144 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24145 });
24146 cx.executor().run_until_parked();
24147 editor.update(cx, |editor, cx| {
24148 assert_eq!(editor.text(cx), "<head></head>");
24149 });
24150}
24151
24152#[gpui::test]
24153async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24154 init_test(cx, |_| {});
24155
24156 let mut cx = EditorTestContext::new(cx).await;
24157 let language = Arc::new(Language::new(
24158 LanguageConfig {
24159 name: "TSX".into(),
24160 matcher: LanguageMatcher {
24161 path_suffixes: vec!["tsx".to_string()],
24162 ..LanguageMatcher::default()
24163 },
24164 brackets: BracketPairConfig {
24165 pairs: vec![BracketPair {
24166 start: "<".into(),
24167 end: ">".into(),
24168 close: true,
24169 ..Default::default()
24170 }],
24171 ..Default::default()
24172 },
24173 linked_edit_characters: HashSet::from_iter(['.']),
24174 ..Default::default()
24175 },
24176 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24177 ));
24178 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24179
24180 // Test typing > does not extend linked pair
24181 cx.set_state("<divˇ<div></div>");
24182 cx.update_editor(|editor, _, cx| {
24183 set_linked_edit_ranges(
24184 (Point::new(0, 1), Point::new(0, 4)),
24185 (Point::new(0, 11), Point::new(0, 14)),
24186 editor,
24187 cx,
24188 );
24189 });
24190 cx.update_editor(|editor, window, cx| {
24191 editor.handle_input(">", window, cx);
24192 });
24193 cx.assert_editor_state("<div>ˇ<div></div>");
24194
24195 // Test typing . do extend linked pair
24196 cx.set_state("<Animatedˇ></Animated>");
24197 cx.update_editor(|editor, _, cx| {
24198 set_linked_edit_ranges(
24199 (Point::new(0, 1), Point::new(0, 9)),
24200 (Point::new(0, 12), Point::new(0, 20)),
24201 editor,
24202 cx,
24203 );
24204 });
24205 cx.update_editor(|editor, window, cx| {
24206 editor.handle_input(".", window, cx);
24207 });
24208 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24209 cx.update_editor(|editor, _, cx| {
24210 set_linked_edit_ranges(
24211 (Point::new(0, 1), Point::new(0, 10)),
24212 (Point::new(0, 13), Point::new(0, 21)),
24213 editor,
24214 cx,
24215 );
24216 });
24217 cx.update_editor(|editor, window, cx| {
24218 editor.handle_input("V", window, cx);
24219 });
24220 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24221}
24222
24223#[gpui::test]
24224async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24225 init_test(cx, |_| {});
24226
24227 let fs = FakeFs::new(cx.executor());
24228 fs.insert_tree(
24229 path!("/root"),
24230 json!({
24231 "a": {
24232 "main.rs": "fn main() {}",
24233 },
24234 "foo": {
24235 "bar": {
24236 "external_file.rs": "pub mod external {}",
24237 }
24238 }
24239 }),
24240 )
24241 .await;
24242
24243 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24244 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24245 language_registry.add(rust_lang());
24246 let _fake_servers = language_registry.register_fake_lsp(
24247 "Rust",
24248 FakeLspAdapter {
24249 ..FakeLspAdapter::default()
24250 },
24251 );
24252 let (workspace, cx) =
24253 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24254 let worktree_id = workspace.update(cx, |workspace, cx| {
24255 workspace.project().update(cx, |project, cx| {
24256 project.worktrees(cx).next().unwrap().read(cx).id()
24257 })
24258 });
24259
24260 let assert_language_servers_count =
24261 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24262 project.update(cx, |project, cx| {
24263 let current = project
24264 .lsp_store()
24265 .read(cx)
24266 .as_local()
24267 .unwrap()
24268 .language_servers
24269 .len();
24270 assert_eq!(expected, current, "{context}");
24271 });
24272 };
24273
24274 assert_language_servers_count(
24275 0,
24276 "No servers should be running before any file is open",
24277 cx,
24278 );
24279 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24280 let main_editor = workspace
24281 .update_in(cx, |workspace, window, cx| {
24282 workspace.open_path(
24283 (worktree_id, rel_path("main.rs")),
24284 Some(pane.downgrade()),
24285 true,
24286 window,
24287 cx,
24288 )
24289 })
24290 .unwrap()
24291 .await
24292 .downcast::<Editor>()
24293 .unwrap();
24294 pane.update(cx, |pane, cx| {
24295 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24296 open_editor.update(cx, |editor, cx| {
24297 assert_eq!(
24298 editor.display_text(cx),
24299 "fn main() {}",
24300 "Original main.rs text on initial open",
24301 );
24302 });
24303 assert_eq!(open_editor, main_editor);
24304 });
24305 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24306
24307 let external_editor = workspace
24308 .update_in(cx, |workspace, window, cx| {
24309 workspace.open_abs_path(
24310 PathBuf::from("/root/foo/bar/external_file.rs"),
24311 OpenOptions::default(),
24312 window,
24313 cx,
24314 )
24315 })
24316 .await
24317 .expect("opening external file")
24318 .downcast::<Editor>()
24319 .expect("downcasted external file's open element to editor");
24320 pane.update(cx, |pane, cx| {
24321 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24322 open_editor.update(cx, |editor, cx| {
24323 assert_eq!(
24324 editor.display_text(cx),
24325 "pub mod external {}",
24326 "External file is open now",
24327 );
24328 });
24329 assert_eq!(open_editor, external_editor);
24330 });
24331 assert_language_servers_count(
24332 1,
24333 "Second, external, *.rs file should join the existing server",
24334 cx,
24335 );
24336
24337 pane.update_in(cx, |pane, window, cx| {
24338 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24339 })
24340 .await
24341 .unwrap();
24342 pane.update_in(cx, |pane, window, cx| {
24343 pane.navigate_backward(&Default::default(), window, cx);
24344 });
24345 cx.run_until_parked();
24346 pane.update(cx, |pane, cx| {
24347 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24348 open_editor.update(cx, |editor, cx| {
24349 assert_eq!(
24350 editor.display_text(cx),
24351 "pub mod external {}",
24352 "External file is open now",
24353 );
24354 });
24355 });
24356 assert_language_servers_count(
24357 1,
24358 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24359 cx,
24360 );
24361
24362 cx.update(|_, cx| {
24363 workspace::reload(cx);
24364 });
24365 assert_language_servers_count(
24366 1,
24367 "After reloading the worktree with local and external files opened, only one project should be started",
24368 cx,
24369 );
24370}
24371
24372#[gpui::test]
24373async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24374 init_test(cx, |_| {});
24375
24376 let mut cx = EditorTestContext::new(cx).await;
24377 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24378 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24379
24380 // test cursor move to start of each line on tab
24381 // for `if`, `elif`, `else`, `while`, `with` and `for`
24382 cx.set_state(indoc! {"
24383 def main():
24384 ˇ for item in items:
24385 ˇ while item.active:
24386 ˇ if item.value > 10:
24387 ˇ continue
24388 ˇ elif item.value < 0:
24389 ˇ break
24390 ˇ else:
24391 ˇ with item.context() as ctx:
24392 ˇ yield count
24393 ˇ else:
24394 ˇ log('while else')
24395 ˇ else:
24396 ˇ log('for else')
24397 "});
24398 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24399 cx.assert_editor_state(indoc! {"
24400 def main():
24401 ˇfor item in items:
24402 ˇwhile item.active:
24403 ˇif item.value > 10:
24404 ˇcontinue
24405 ˇelif item.value < 0:
24406 ˇbreak
24407 ˇelse:
24408 ˇwith item.context() as ctx:
24409 ˇyield count
24410 ˇelse:
24411 ˇlog('while else')
24412 ˇelse:
24413 ˇlog('for else')
24414 "});
24415 // test relative indent is preserved when tab
24416 // for `if`, `elif`, `else`, `while`, `with` and `for`
24417 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24418 cx.assert_editor_state(indoc! {"
24419 def main():
24420 ˇfor item in items:
24421 ˇwhile item.active:
24422 ˇif item.value > 10:
24423 ˇcontinue
24424 ˇelif item.value < 0:
24425 ˇbreak
24426 ˇelse:
24427 ˇwith item.context() as ctx:
24428 ˇyield count
24429 ˇelse:
24430 ˇlog('while else')
24431 ˇelse:
24432 ˇlog('for else')
24433 "});
24434
24435 // test cursor move to start of each line on tab
24436 // for `try`, `except`, `else`, `finally`, `match` and `def`
24437 cx.set_state(indoc! {"
24438 def main():
24439 ˇ try:
24440 ˇ fetch()
24441 ˇ except ValueError:
24442 ˇ handle_error()
24443 ˇ else:
24444 ˇ match value:
24445 ˇ case _:
24446 ˇ finally:
24447 ˇ def status():
24448 ˇ return 0
24449 "});
24450 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24451 cx.assert_editor_state(indoc! {"
24452 def main():
24453 ˇtry:
24454 ˇfetch()
24455 ˇexcept ValueError:
24456 ˇhandle_error()
24457 ˇelse:
24458 ˇmatch value:
24459 ˇcase _:
24460 ˇfinally:
24461 ˇdef status():
24462 ˇreturn 0
24463 "});
24464 // test relative indent is preserved when tab
24465 // for `try`, `except`, `else`, `finally`, `match` and `def`
24466 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24467 cx.assert_editor_state(indoc! {"
24468 def main():
24469 ˇtry:
24470 ˇfetch()
24471 ˇexcept ValueError:
24472 ˇhandle_error()
24473 ˇelse:
24474 ˇmatch value:
24475 ˇcase _:
24476 ˇfinally:
24477 ˇdef status():
24478 ˇreturn 0
24479 "});
24480}
24481
24482#[gpui::test]
24483async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24484 init_test(cx, |_| {});
24485
24486 let mut cx = EditorTestContext::new(cx).await;
24487 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24488 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24489
24490 // test `else` auto outdents when typed inside `if` block
24491 cx.set_state(indoc! {"
24492 def main():
24493 if i == 2:
24494 return
24495 ˇ
24496 "});
24497 cx.update_editor(|editor, window, cx| {
24498 editor.handle_input("else:", window, cx);
24499 });
24500 cx.assert_editor_state(indoc! {"
24501 def main():
24502 if i == 2:
24503 return
24504 else:ˇ
24505 "});
24506
24507 // test `except` auto outdents when typed inside `try` block
24508 cx.set_state(indoc! {"
24509 def main():
24510 try:
24511 i = 2
24512 ˇ
24513 "});
24514 cx.update_editor(|editor, window, cx| {
24515 editor.handle_input("except:", window, cx);
24516 });
24517 cx.assert_editor_state(indoc! {"
24518 def main():
24519 try:
24520 i = 2
24521 except:ˇ
24522 "});
24523
24524 // test `else` auto outdents when typed inside `except` block
24525 cx.set_state(indoc! {"
24526 def main():
24527 try:
24528 i = 2
24529 except:
24530 j = 2
24531 ˇ
24532 "});
24533 cx.update_editor(|editor, window, cx| {
24534 editor.handle_input("else:", window, cx);
24535 });
24536 cx.assert_editor_state(indoc! {"
24537 def main():
24538 try:
24539 i = 2
24540 except:
24541 j = 2
24542 else:ˇ
24543 "});
24544
24545 // test `finally` auto outdents when typed inside `else` block
24546 cx.set_state(indoc! {"
24547 def main():
24548 try:
24549 i = 2
24550 except:
24551 j = 2
24552 else:
24553 k = 2
24554 ˇ
24555 "});
24556 cx.update_editor(|editor, window, cx| {
24557 editor.handle_input("finally:", window, cx);
24558 });
24559 cx.assert_editor_state(indoc! {"
24560 def main():
24561 try:
24562 i = 2
24563 except:
24564 j = 2
24565 else:
24566 k = 2
24567 finally:ˇ
24568 "});
24569
24570 // test `else` does not outdents when typed inside `except` block right after for block
24571 cx.set_state(indoc! {"
24572 def main():
24573 try:
24574 i = 2
24575 except:
24576 for i in range(n):
24577 pass
24578 ˇ
24579 "});
24580 cx.update_editor(|editor, window, cx| {
24581 editor.handle_input("else:", window, cx);
24582 });
24583 cx.assert_editor_state(indoc! {"
24584 def main():
24585 try:
24586 i = 2
24587 except:
24588 for i in range(n):
24589 pass
24590 else:ˇ
24591 "});
24592
24593 // test `finally` auto outdents when typed inside `else` block right after for block
24594 cx.set_state(indoc! {"
24595 def main():
24596 try:
24597 i = 2
24598 except:
24599 j = 2
24600 else:
24601 for i in range(n):
24602 pass
24603 ˇ
24604 "});
24605 cx.update_editor(|editor, window, cx| {
24606 editor.handle_input("finally:", window, cx);
24607 });
24608 cx.assert_editor_state(indoc! {"
24609 def main():
24610 try:
24611 i = 2
24612 except:
24613 j = 2
24614 else:
24615 for i in range(n):
24616 pass
24617 finally:ˇ
24618 "});
24619
24620 // test `except` outdents to inner "try" block
24621 cx.set_state(indoc! {"
24622 def main():
24623 try:
24624 i = 2
24625 if i == 2:
24626 try:
24627 i = 3
24628 ˇ
24629 "});
24630 cx.update_editor(|editor, window, cx| {
24631 editor.handle_input("except:", window, cx);
24632 });
24633 cx.assert_editor_state(indoc! {"
24634 def main():
24635 try:
24636 i = 2
24637 if i == 2:
24638 try:
24639 i = 3
24640 except:ˇ
24641 "});
24642
24643 // test `except` outdents to outer "try" block
24644 cx.set_state(indoc! {"
24645 def main():
24646 try:
24647 i = 2
24648 if i == 2:
24649 try:
24650 i = 3
24651 ˇ
24652 "});
24653 cx.update_editor(|editor, window, cx| {
24654 editor.handle_input("except:", window, cx);
24655 });
24656 cx.assert_editor_state(indoc! {"
24657 def main():
24658 try:
24659 i = 2
24660 if i == 2:
24661 try:
24662 i = 3
24663 except:ˇ
24664 "});
24665
24666 // test `else` stays at correct indent when typed after `for` block
24667 cx.set_state(indoc! {"
24668 def main():
24669 for i in range(10):
24670 if i == 3:
24671 break
24672 ˇ
24673 "});
24674 cx.update_editor(|editor, window, cx| {
24675 editor.handle_input("else:", window, cx);
24676 });
24677 cx.assert_editor_state(indoc! {"
24678 def main():
24679 for i in range(10):
24680 if i == 3:
24681 break
24682 else:ˇ
24683 "});
24684
24685 // test does not outdent on typing after line with square brackets
24686 cx.set_state(indoc! {"
24687 def f() -> list[str]:
24688 ˇ
24689 "});
24690 cx.update_editor(|editor, window, cx| {
24691 editor.handle_input("a", window, cx);
24692 });
24693 cx.assert_editor_state(indoc! {"
24694 def f() -> list[str]:
24695 aˇ
24696 "});
24697
24698 // test does not outdent on typing : after case keyword
24699 cx.set_state(indoc! {"
24700 match 1:
24701 caseˇ
24702 "});
24703 cx.update_editor(|editor, window, cx| {
24704 editor.handle_input(":", window, cx);
24705 });
24706 cx.assert_editor_state(indoc! {"
24707 match 1:
24708 case:ˇ
24709 "});
24710}
24711
24712#[gpui::test]
24713async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24714 init_test(cx, |_| {});
24715 update_test_language_settings(cx, |settings| {
24716 settings.defaults.extend_comment_on_newline = Some(false);
24717 });
24718 let mut cx = EditorTestContext::new(cx).await;
24719 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24720 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24721
24722 // test correct indent after newline on comment
24723 cx.set_state(indoc! {"
24724 # COMMENT:ˇ
24725 "});
24726 cx.update_editor(|editor, window, cx| {
24727 editor.newline(&Newline, window, cx);
24728 });
24729 cx.assert_editor_state(indoc! {"
24730 # COMMENT:
24731 ˇ
24732 "});
24733
24734 // test correct indent after newline in brackets
24735 cx.set_state(indoc! {"
24736 {ˇ}
24737 "});
24738 cx.update_editor(|editor, window, cx| {
24739 editor.newline(&Newline, window, cx);
24740 });
24741 cx.run_until_parked();
24742 cx.assert_editor_state(indoc! {"
24743 {
24744 ˇ
24745 }
24746 "});
24747
24748 cx.set_state(indoc! {"
24749 (ˇ)
24750 "});
24751 cx.update_editor(|editor, window, cx| {
24752 editor.newline(&Newline, window, cx);
24753 });
24754 cx.run_until_parked();
24755 cx.assert_editor_state(indoc! {"
24756 (
24757 ˇ
24758 )
24759 "});
24760
24761 // do not indent after empty lists or dictionaries
24762 cx.set_state(indoc! {"
24763 a = []ˇ
24764 "});
24765 cx.update_editor(|editor, window, cx| {
24766 editor.newline(&Newline, window, cx);
24767 });
24768 cx.run_until_parked();
24769 cx.assert_editor_state(indoc! {"
24770 a = []
24771 ˇ
24772 "});
24773}
24774
24775#[gpui::test]
24776async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24777 init_test(cx, |_| {});
24778
24779 let mut cx = EditorTestContext::new(cx).await;
24780 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24781 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24782
24783 // test cursor move to start of each line on tab
24784 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24785 cx.set_state(indoc! {"
24786 function main() {
24787 ˇ for item in $items; do
24788 ˇ while [ -n \"$item\" ]; do
24789 ˇ if [ \"$value\" -gt 10 ]; then
24790 ˇ continue
24791 ˇ elif [ \"$value\" -lt 0 ]; then
24792 ˇ break
24793 ˇ else
24794 ˇ echo \"$item\"
24795 ˇ fi
24796 ˇ done
24797 ˇ done
24798 ˇ}
24799 "});
24800 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24801 cx.assert_editor_state(indoc! {"
24802 function main() {
24803 ˇfor item in $items; do
24804 ˇwhile [ -n \"$item\" ]; do
24805 ˇif [ \"$value\" -gt 10 ]; then
24806 ˇcontinue
24807 ˇelif [ \"$value\" -lt 0 ]; then
24808 ˇbreak
24809 ˇelse
24810 ˇecho \"$item\"
24811 ˇfi
24812 ˇdone
24813 ˇdone
24814 ˇ}
24815 "});
24816 // test relative indent is preserved when tab
24817 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24818 cx.assert_editor_state(indoc! {"
24819 function main() {
24820 ˇfor item in $items; do
24821 ˇwhile [ -n \"$item\" ]; do
24822 ˇif [ \"$value\" -gt 10 ]; then
24823 ˇcontinue
24824 ˇelif [ \"$value\" -lt 0 ]; then
24825 ˇbreak
24826 ˇelse
24827 ˇecho \"$item\"
24828 ˇfi
24829 ˇdone
24830 ˇdone
24831 ˇ}
24832 "});
24833
24834 // test cursor move to start of each line on tab
24835 // for `case` statement with patterns
24836 cx.set_state(indoc! {"
24837 function handle() {
24838 ˇ case \"$1\" in
24839 ˇ start)
24840 ˇ echo \"a\"
24841 ˇ ;;
24842 ˇ stop)
24843 ˇ echo \"b\"
24844 ˇ ;;
24845 ˇ *)
24846 ˇ echo \"c\"
24847 ˇ ;;
24848 ˇ esac
24849 ˇ}
24850 "});
24851 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24852 cx.assert_editor_state(indoc! {"
24853 function handle() {
24854 ˇcase \"$1\" in
24855 ˇstart)
24856 ˇecho \"a\"
24857 ˇ;;
24858 ˇstop)
24859 ˇecho \"b\"
24860 ˇ;;
24861 ˇ*)
24862 ˇecho \"c\"
24863 ˇ;;
24864 ˇesac
24865 ˇ}
24866 "});
24867}
24868
24869#[gpui::test]
24870async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24871 init_test(cx, |_| {});
24872
24873 let mut cx = EditorTestContext::new(cx).await;
24874 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24875 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24876
24877 // test indents on comment insert
24878 cx.set_state(indoc! {"
24879 function main() {
24880 ˇ for item in $items; do
24881 ˇ while [ -n \"$item\" ]; do
24882 ˇ if [ \"$value\" -gt 10 ]; then
24883 ˇ continue
24884 ˇ elif [ \"$value\" -lt 0 ]; then
24885 ˇ break
24886 ˇ else
24887 ˇ echo \"$item\"
24888 ˇ fi
24889 ˇ done
24890 ˇ done
24891 ˇ}
24892 "});
24893 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24894 cx.assert_editor_state(indoc! {"
24895 function main() {
24896 #ˇ for item in $items; do
24897 #ˇ while [ -n \"$item\" ]; do
24898 #ˇ if [ \"$value\" -gt 10 ]; then
24899 #ˇ continue
24900 #ˇ elif [ \"$value\" -lt 0 ]; then
24901 #ˇ break
24902 #ˇ else
24903 #ˇ echo \"$item\"
24904 #ˇ fi
24905 #ˇ done
24906 #ˇ done
24907 #ˇ}
24908 "});
24909}
24910
24911#[gpui::test]
24912async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24913 init_test(cx, |_| {});
24914
24915 let mut cx = EditorTestContext::new(cx).await;
24916 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24917 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24918
24919 // test `else` auto outdents when typed inside `if` block
24920 cx.set_state(indoc! {"
24921 if [ \"$1\" = \"test\" ]; then
24922 echo \"foo bar\"
24923 ˇ
24924 "});
24925 cx.update_editor(|editor, window, cx| {
24926 editor.handle_input("else", window, cx);
24927 });
24928 cx.assert_editor_state(indoc! {"
24929 if [ \"$1\" = \"test\" ]; then
24930 echo \"foo bar\"
24931 elseˇ
24932 "});
24933
24934 // test `elif` auto outdents when typed inside `if` block
24935 cx.set_state(indoc! {"
24936 if [ \"$1\" = \"test\" ]; then
24937 echo \"foo bar\"
24938 ˇ
24939 "});
24940 cx.update_editor(|editor, window, cx| {
24941 editor.handle_input("elif", window, cx);
24942 });
24943 cx.assert_editor_state(indoc! {"
24944 if [ \"$1\" = \"test\" ]; then
24945 echo \"foo bar\"
24946 elifˇ
24947 "});
24948
24949 // test `fi` auto outdents when typed inside `else` block
24950 cx.set_state(indoc! {"
24951 if [ \"$1\" = \"test\" ]; then
24952 echo \"foo bar\"
24953 else
24954 echo \"bar baz\"
24955 ˇ
24956 "});
24957 cx.update_editor(|editor, window, cx| {
24958 editor.handle_input("fi", window, cx);
24959 });
24960 cx.assert_editor_state(indoc! {"
24961 if [ \"$1\" = \"test\" ]; then
24962 echo \"foo bar\"
24963 else
24964 echo \"bar baz\"
24965 fiˇ
24966 "});
24967
24968 // test `done` auto outdents when typed inside `while` block
24969 cx.set_state(indoc! {"
24970 while read line; do
24971 echo \"$line\"
24972 ˇ
24973 "});
24974 cx.update_editor(|editor, window, cx| {
24975 editor.handle_input("done", window, cx);
24976 });
24977 cx.assert_editor_state(indoc! {"
24978 while read line; do
24979 echo \"$line\"
24980 doneˇ
24981 "});
24982
24983 // test `done` auto outdents when typed inside `for` block
24984 cx.set_state(indoc! {"
24985 for file in *.txt; do
24986 cat \"$file\"
24987 ˇ
24988 "});
24989 cx.update_editor(|editor, window, cx| {
24990 editor.handle_input("done", window, cx);
24991 });
24992 cx.assert_editor_state(indoc! {"
24993 for file in *.txt; do
24994 cat \"$file\"
24995 doneˇ
24996 "});
24997
24998 // test `esac` auto outdents when typed inside `case` block
24999 cx.set_state(indoc! {"
25000 case \"$1\" in
25001 start)
25002 echo \"foo bar\"
25003 ;;
25004 stop)
25005 echo \"bar baz\"
25006 ;;
25007 ˇ
25008 "});
25009 cx.update_editor(|editor, window, cx| {
25010 editor.handle_input("esac", window, cx);
25011 });
25012 cx.assert_editor_state(indoc! {"
25013 case \"$1\" in
25014 start)
25015 echo \"foo bar\"
25016 ;;
25017 stop)
25018 echo \"bar baz\"
25019 ;;
25020 esacˇ
25021 "});
25022
25023 // test `*)` auto outdents when typed inside `case` block
25024 cx.set_state(indoc! {"
25025 case \"$1\" in
25026 start)
25027 echo \"foo bar\"
25028 ;;
25029 ˇ
25030 "});
25031 cx.update_editor(|editor, window, cx| {
25032 editor.handle_input("*)", window, cx);
25033 });
25034 cx.assert_editor_state(indoc! {"
25035 case \"$1\" in
25036 start)
25037 echo \"foo bar\"
25038 ;;
25039 *)ˇ
25040 "});
25041
25042 // test `fi` outdents to correct level with nested if blocks
25043 cx.set_state(indoc! {"
25044 if [ \"$1\" = \"test\" ]; then
25045 echo \"outer if\"
25046 if [ \"$2\" = \"debug\" ]; then
25047 echo \"inner if\"
25048 ˇ
25049 "});
25050 cx.update_editor(|editor, window, cx| {
25051 editor.handle_input("fi", window, cx);
25052 });
25053 cx.assert_editor_state(indoc! {"
25054 if [ \"$1\" = \"test\" ]; then
25055 echo \"outer if\"
25056 if [ \"$2\" = \"debug\" ]; then
25057 echo \"inner if\"
25058 fiˇ
25059 "});
25060}
25061
25062#[gpui::test]
25063async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25064 init_test(cx, |_| {});
25065 update_test_language_settings(cx, |settings| {
25066 settings.defaults.extend_comment_on_newline = Some(false);
25067 });
25068 let mut cx = EditorTestContext::new(cx).await;
25069 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25070 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25071
25072 // test correct indent after newline on comment
25073 cx.set_state(indoc! {"
25074 # COMMENT:ˇ
25075 "});
25076 cx.update_editor(|editor, window, cx| {
25077 editor.newline(&Newline, window, cx);
25078 });
25079 cx.assert_editor_state(indoc! {"
25080 # COMMENT:
25081 ˇ
25082 "});
25083
25084 // test correct indent after newline after `then`
25085 cx.set_state(indoc! {"
25086
25087 if [ \"$1\" = \"test\" ]; thenˇ
25088 "});
25089 cx.update_editor(|editor, window, cx| {
25090 editor.newline(&Newline, window, cx);
25091 });
25092 cx.run_until_parked();
25093 cx.assert_editor_state(indoc! {"
25094
25095 if [ \"$1\" = \"test\" ]; then
25096 ˇ
25097 "});
25098
25099 // test correct indent after newline after `else`
25100 cx.set_state(indoc! {"
25101 if [ \"$1\" = \"test\" ]; then
25102 elseˇ
25103 "});
25104 cx.update_editor(|editor, window, cx| {
25105 editor.newline(&Newline, window, cx);
25106 });
25107 cx.run_until_parked();
25108 cx.assert_editor_state(indoc! {"
25109 if [ \"$1\" = \"test\" ]; then
25110 else
25111 ˇ
25112 "});
25113
25114 // test correct indent after newline after `elif`
25115 cx.set_state(indoc! {"
25116 if [ \"$1\" = \"test\" ]; then
25117 elifˇ
25118 "});
25119 cx.update_editor(|editor, window, cx| {
25120 editor.newline(&Newline, window, cx);
25121 });
25122 cx.run_until_parked();
25123 cx.assert_editor_state(indoc! {"
25124 if [ \"$1\" = \"test\" ]; then
25125 elif
25126 ˇ
25127 "});
25128
25129 // test correct indent after newline after `do`
25130 cx.set_state(indoc! {"
25131 for file in *.txt; doˇ
25132 "});
25133 cx.update_editor(|editor, window, cx| {
25134 editor.newline(&Newline, window, cx);
25135 });
25136 cx.run_until_parked();
25137 cx.assert_editor_state(indoc! {"
25138 for file in *.txt; do
25139 ˇ
25140 "});
25141
25142 // test correct indent after newline after case pattern
25143 cx.set_state(indoc! {"
25144 case \"$1\" in
25145 start)ˇ
25146 "});
25147 cx.update_editor(|editor, window, cx| {
25148 editor.newline(&Newline, window, cx);
25149 });
25150 cx.run_until_parked();
25151 cx.assert_editor_state(indoc! {"
25152 case \"$1\" in
25153 start)
25154 ˇ
25155 "});
25156
25157 // test correct indent after newline after case pattern
25158 cx.set_state(indoc! {"
25159 case \"$1\" in
25160 start)
25161 ;;
25162 *)ˇ
25163 "});
25164 cx.update_editor(|editor, window, cx| {
25165 editor.newline(&Newline, window, cx);
25166 });
25167 cx.run_until_parked();
25168 cx.assert_editor_state(indoc! {"
25169 case \"$1\" in
25170 start)
25171 ;;
25172 *)
25173 ˇ
25174 "});
25175
25176 // test correct indent after newline after function opening brace
25177 cx.set_state(indoc! {"
25178 function test() {ˇ}
25179 "});
25180 cx.update_editor(|editor, window, cx| {
25181 editor.newline(&Newline, window, cx);
25182 });
25183 cx.run_until_parked();
25184 cx.assert_editor_state(indoc! {"
25185 function test() {
25186 ˇ
25187 }
25188 "});
25189
25190 // test no extra indent after semicolon on same line
25191 cx.set_state(indoc! {"
25192 echo \"test\";ˇ
25193 "});
25194 cx.update_editor(|editor, window, cx| {
25195 editor.newline(&Newline, window, cx);
25196 });
25197 cx.run_until_parked();
25198 cx.assert_editor_state(indoc! {"
25199 echo \"test\";
25200 ˇ
25201 "});
25202}
25203
25204fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25205 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25206 point..point
25207}
25208
25209#[track_caller]
25210fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25211 let (text, ranges) = marked_text_ranges(marked_text, true);
25212 assert_eq!(editor.text(cx), text);
25213 assert_eq!(
25214 editor.selections.ranges(cx),
25215 ranges,
25216 "Assert selections are {}",
25217 marked_text
25218 );
25219}
25220
25221pub fn handle_signature_help_request(
25222 cx: &mut EditorLspTestContext,
25223 mocked_response: lsp::SignatureHelp,
25224) -> impl Future<Output = ()> + use<> {
25225 let mut request =
25226 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25227 let mocked_response = mocked_response.clone();
25228 async move { Ok(Some(mocked_response)) }
25229 });
25230
25231 async move {
25232 request.next().await;
25233 }
25234}
25235
25236#[track_caller]
25237pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25238 cx.update_editor(|editor, _, _| {
25239 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25240 let entries = menu.entries.borrow();
25241 let entries = entries
25242 .iter()
25243 .map(|entry| entry.string.as_str())
25244 .collect::<Vec<_>>();
25245 assert_eq!(entries, expected);
25246 } else {
25247 panic!("Expected completions menu");
25248 }
25249 });
25250}
25251
25252/// Handle completion request passing a marked string specifying where the completion
25253/// should be triggered from using '|' character, what range should be replaced, and what completions
25254/// should be returned using '<' and '>' to delimit the range.
25255///
25256/// Also see `handle_completion_request_with_insert_and_replace`.
25257#[track_caller]
25258pub fn handle_completion_request(
25259 marked_string: &str,
25260 completions: Vec<&'static str>,
25261 is_incomplete: bool,
25262 counter: Arc<AtomicUsize>,
25263 cx: &mut EditorLspTestContext,
25264) -> impl Future<Output = ()> {
25265 let complete_from_marker: TextRangeMarker = '|'.into();
25266 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25267 let (_, mut marked_ranges) = marked_text_ranges_by(
25268 marked_string,
25269 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25270 );
25271
25272 let complete_from_position =
25273 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25274 let replace_range =
25275 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25276
25277 let mut request =
25278 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25279 let completions = completions.clone();
25280 counter.fetch_add(1, atomic::Ordering::Release);
25281 async move {
25282 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25283 assert_eq!(
25284 params.text_document_position.position,
25285 complete_from_position
25286 );
25287 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25288 is_incomplete,
25289 item_defaults: None,
25290 items: completions
25291 .iter()
25292 .map(|completion_text| lsp::CompletionItem {
25293 label: completion_text.to_string(),
25294 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25295 range: replace_range,
25296 new_text: completion_text.to_string(),
25297 })),
25298 ..Default::default()
25299 })
25300 .collect(),
25301 })))
25302 }
25303 });
25304
25305 async move {
25306 request.next().await;
25307 }
25308}
25309
25310/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25311/// given instead, which also contains an `insert` range.
25312///
25313/// This function uses markers to define ranges:
25314/// - `|` marks the cursor position
25315/// - `<>` marks the replace range
25316/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25317pub fn handle_completion_request_with_insert_and_replace(
25318 cx: &mut EditorLspTestContext,
25319 marked_string: &str,
25320 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25321 counter: Arc<AtomicUsize>,
25322) -> impl Future<Output = ()> {
25323 let complete_from_marker: TextRangeMarker = '|'.into();
25324 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25325 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25326
25327 let (_, mut marked_ranges) = marked_text_ranges_by(
25328 marked_string,
25329 vec![
25330 complete_from_marker.clone(),
25331 replace_range_marker.clone(),
25332 insert_range_marker.clone(),
25333 ],
25334 );
25335
25336 let complete_from_position =
25337 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25338 let replace_range =
25339 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25340
25341 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25342 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25343 _ => lsp::Range {
25344 start: replace_range.start,
25345 end: complete_from_position,
25346 },
25347 };
25348
25349 let mut request =
25350 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25351 let completions = completions.clone();
25352 counter.fetch_add(1, atomic::Ordering::Release);
25353 async move {
25354 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25355 assert_eq!(
25356 params.text_document_position.position, complete_from_position,
25357 "marker `|` position doesn't match",
25358 );
25359 Ok(Some(lsp::CompletionResponse::Array(
25360 completions
25361 .iter()
25362 .map(|(label, new_text)| lsp::CompletionItem {
25363 label: label.to_string(),
25364 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25365 lsp::InsertReplaceEdit {
25366 insert: insert_range,
25367 replace: replace_range,
25368 new_text: new_text.to_string(),
25369 },
25370 )),
25371 ..Default::default()
25372 })
25373 .collect(),
25374 )))
25375 }
25376 });
25377
25378 async move {
25379 request.next().await;
25380 }
25381}
25382
25383fn handle_resolve_completion_request(
25384 cx: &mut EditorLspTestContext,
25385 edits: Option<Vec<(&'static str, &'static str)>>,
25386) -> impl Future<Output = ()> {
25387 let edits = edits.map(|edits| {
25388 edits
25389 .iter()
25390 .map(|(marked_string, new_text)| {
25391 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25392 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25393 lsp::TextEdit::new(replace_range, new_text.to_string())
25394 })
25395 .collect::<Vec<_>>()
25396 });
25397
25398 let mut request =
25399 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25400 let edits = edits.clone();
25401 async move {
25402 Ok(lsp::CompletionItem {
25403 additional_text_edits: edits,
25404 ..Default::default()
25405 })
25406 }
25407 });
25408
25409 async move {
25410 request.next().await;
25411 }
25412}
25413
25414pub(crate) fn update_test_language_settings(
25415 cx: &mut TestAppContext,
25416 f: impl Fn(&mut AllLanguageSettingsContent),
25417) {
25418 cx.update(|cx| {
25419 SettingsStore::update_global(cx, |store, cx| {
25420 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25421 });
25422 });
25423}
25424
25425pub(crate) fn update_test_project_settings(
25426 cx: &mut TestAppContext,
25427 f: impl Fn(&mut ProjectSettingsContent),
25428) {
25429 cx.update(|cx| {
25430 SettingsStore::update_global(cx, |store, cx| {
25431 store.update_user_settings(cx, |settings| f(&mut settings.project));
25432 });
25433 });
25434}
25435
25436pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25437 cx.update(|cx| {
25438 assets::Assets.load_test_fonts(cx);
25439 let store = SettingsStore::test(cx);
25440 cx.set_global(store);
25441 theme::init(theme::LoadThemes::JustBase, cx);
25442 release_channel::init(SemanticVersion::default(), cx);
25443 client::init_settings(cx);
25444 language::init(cx);
25445 Project::init_settings(cx);
25446 workspace::init_settings(cx);
25447 crate::init(cx);
25448 });
25449 zlog::init_test();
25450 update_test_language_settings(cx, f);
25451}
25452
25453#[track_caller]
25454fn assert_hunk_revert(
25455 not_reverted_text_with_selections: &str,
25456 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25457 expected_reverted_text_with_selections: &str,
25458 base_text: &str,
25459 cx: &mut EditorLspTestContext,
25460) {
25461 cx.set_state(not_reverted_text_with_selections);
25462 cx.set_head_text(base_text);
25463 cx.executor().run_until_parked();
25464
25465 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25466 let snapshot = editor.snapshot(window, cx);
25467 let reverted_hunk_statuses = snapshot
25468 .buffer_snapshot()
25469 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
25470 .map(|hunk| hunk.status().kind)
25471 .collect::<Vec<_>>();
25472
25473 editor.git_restore(&Default::default(), window, cx);
25474 reverted_hunk_statuses
25475 });
25476 cx.executor().run_until_parked();
25477 cx.assert_editor_state(expected_reverted_text_with_selections);
25478 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25479}
25480
25481#[gpui::test(iterations = 10)]
25482async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25483 init_test(cx, |_| {});
25484
25485 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25486 let counter = diagnostic_requests.clone();
25487
25488 let fs = FakeFs::new(cx.executor());
25489 fs.insert_tree(
25490 path!("/a"),
25491 json!({
25492 "first.rs": "fn main() { let a = 5; }",
25493 "second.rs": "// Test file",
25494 }),
25495 )
25496 .await;
25497
25498 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25499 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25500 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25501
25502 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25503 language_registry.add(rust_lang());
25504 let mut fake_servers = language_registry.register_fake_lsp(
25505 "Rust",
25506 FakeLspAdapter {
25507 capabilities: lsp::ServerCapabilities {
25508 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25509 lsp::DiagnosticOptions {
25510 identifier: None,
25511 inter_file_dependencies: true,
25512 workspace_diagnostics: true,
25513 work_done_progress_options: Default::default(),
25514 },
25515 )),
25516 ..Default::default()
25517 },
25518 ..Default::default()
25519 },
25520 );
25521
25522 let editor = workspace
25523 .update(cx, |workspace, window, cx| {
25524 workspace.open_abs_path(
25525 PathBuf::from(path!("/a/first.rs")),
25526 OpenOptions::default(),
25527 window,
25528 cx,
25529 )
25530 })
25531 .unwrap()
25532 .await
25533 .unwrap()
25534 .downcast::<Editor>()
25535 .unwrap();
25536 let fake_server = fake_servers.next().await.unwrap();
25537 let server_id = fake_server.server.server_id();
25538 let mut first_request = fake_server
25539 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25540 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25541 let result_id = Some(new_result_id.to_string());
25542 assert_eq!(
25543 params.text_document.uri,
25544 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25545 );
25546 async move {
25547 Ok(lsp::DocumentDiagnosticReportResult::Report(
25548 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25549 related_documents: None,
25550 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25551 items: Vec::new(),
25552 result_id,
25553 },
25554 }),
25555 ))
25556 }
25557 });
25558
25559 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25560 project.update(cx, |project, cx| {
25561 let buffer_id = editor
25562 .read(cx)
25563 .buffer()
25564 .read(cx)
25565 .as_singleton()
25566 .expect("created a singleton buffer")
25567 .read(cx)
25568 .remote_id();
25569 let buffer_result_id = project
25570 .lsp_store()
25571 .read(cx)
25572 .result_id(server_id, buffer_id, cx);
25573 assert_eq!(expected, buffer_result_id);
25574 });
25575 };
25576
25577 ensure_result_id(None, cx);
25578 cx.executor().advance_clock(Duration::from_millis(60));
25579 cx.executor().run_until_parked();
25580 assert_eq!(
25581 diagnostic_requests.load(atomic::Ordering::Acquire),
25582 1,
25583 "Opening file should trigger diagnostic request"
25584 );
25585 first_request
25586 .next()
25587 .await
25588 .expect("should have sent the first diagnostics pull request");
25589 ensure_result_id(Some("1".to_string()), cx);
25590
25591 // Editing should trigger diagnostics
25592 editor.update_in(cx, |editor, window, cx| {
25593 editor.handle_input("2", window, cx)
25594 });
25595 cx.executor().advance_clock(Duration::from_millis(60));
25596 cx.executor().run_until_parked();
25597 assert_eq!(
25598 diagnostic_requests.load(atomic::Ordering::Acquire),
25599 2,
25600 "Editing should trigger diagnostic request"
25601 );
25602 ensure_result_id(Some("2".to_string()), cx);
25603
25604 // Moving cursor should not trigger diagnostic request
25605 editor.update_in(cx, |editor, window, cx| {
25606 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25607 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25608 });
25609 });
25610 cx.executor().advance_clock(Duration::from_millis(60));
25611 cx.executor().run_until_parked();
25612 assert_eq!(
25613 diagnostic_requests.load(atomic::Ordering::Acquire),
25614 2,
25615 "Cursor movement should not trigger diagnostic request"
25616 );
25617 ensure_result_id(Some("2".to_string()), cx);
25618 // Multiple rapid edits should be debounced
25619 for _ in 0..5 {
25620 editor.update_in(cx, |editor, window, cx| {
25621 editor.handle_input("x", window, cx)
25622 });
25623 }
25624 cx.executor().advance_clock(Duration::from_millis(60));
25625 cx.executor().run_until_parked();
25626
25627 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25628 assert!(
25629 final_requests <= 4,
25630 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25631 );
25632 ensure_result_id(Some(final_requests.to_string()), cx);
25633}
25634
25635#[gpui::test]
25636async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25637 // Regression test for issue #11671
25638 // Previously, adding a cursor after moving multiple cursors would reset
25639 // the cursor count instead of adding to the existing cursors.
25640 init_test(cx, |_| {});
25641 let mut cx = EditorTestContext::new(cx).await;
25642
25643 // Create a simple buffer with cursor at start
25644 cx.set_state(indoc! {"
25645 ˇaaaa
25646 bbbb
25647 cccc
25648 dddd
25649 eeee
25650 ffff
25651 gggg
25652 hhhh"});
25653
25654 // Add 2 cursors below (so we have 3 total)
25655 cx.update_editor(|editor, window, cx| {
25656 editor.add_selection_below(&Default::default(), window, cx);
25657 editor.add_selection_below(&Default::default(), window, cx);
25658 });
25659
25660 // Verify we have 3 cursors
25661 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25662 assert_eq!(
25663 initial_count, 3,
25664 "Should have 3 cursors after adding 2 below"
25665 );
25666
25667 // Move down one line
25668 cx.update_editor(|editor, window, cx| {
25669 editor.move_down(&MoveDown, window, cx);
25670 });
25671
25672 // Add another cursor below
25673 cx.update_editor(|editor, window, cx| {
25674 editor.add_selection_below(&Default::default(), window, cx);
25675 });
25676
25677 // Should now have 4 cursors (3 original + 1 new)
25678 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25679 assert_eq!(
25680 final_count, 4,
25681 "Should have 4 cursors after moving and adding another"
25682 );
25683}
25684
25685#[gpui::test]
25686async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
25687 init_test(cx, |_| {});
25688
25689 let mut cx = EditorTestContext::new(cx).await;
25690
25691 cx.set_state(indoc!(
25692 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
25693 Second line here"#
25694 ));
25695
25696 cx.update_editor(|editor, window, cx| {
25697 // Enable soft wrapping with a narrow width to force soft wrapping and
25698 // confirm that more than 2 rows are being displayed.
25699 editor.set_wrap_width(Some(100.0.into()), cx);
25700 assert!(editor.display_text(cx).lines().count() > 2);
25701
25702 editor.add_selection_below(
25703 &AddSelectionBelow {
25704 skip_soft_wrap: true,
25705 },
25706 window,
25707 cx,
25708 );
25709
25710 assert_eq!(
25711 editor.selections.display_ranges(cx),
25712 &[
25713 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
25714 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
25715 ]
25716 );
25717
25718 editor.add_selection_above(
25719 &AddSelectionAbove {
25720 skip_soft_wrap: true,
25721 },
25722 window,
25723 cx,
25724 );
25725
25726 assert_eq!(
25727 editor.selections.display_ranges(cx),
25728 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
25729 );
25730
25731 editor.add_selection_below(
25732 &AddSelectionBelow {
25733 skip_soft_wrap: false,
25734 },
25735 window,
25736 cx,
25737 );
25738
25739 assert_eq!(
25740 editor.selections.display_ranges(cx),
25741 &[
25742 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
25743 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
25744 ]
25745 );
25746
25747 editor.add_selection_above(
25748 &AddSelectionAbove {
25749 skip_soft_wrap: false,
25750 },
25751 window,
25752 cx,
25753 );
25754
25755 assert_eq!(
25756 editor.selections.display_ranges(cx),
25757 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
25758 );
25759 });
25760}
25761
25762#[gpui::test(iterations = 10)]
25763async fn test_document_colors(cx: &mut TestAppContext) {
25764 let expected_color = Rgba {
25765 r: 0.33,
25766 g: 0.33,
25767 b: 0.33,
25768 a: 0.33,
25769 };
25770
25771 init_test(cx, |_| {});
25772
25773 let fs = FakeFs::new(cx.executor());
25774 fs.insert_tree(
25775 path!("/a"),
25776 json!({
25777 "first.rs": "fn main() { let a = 5; }",
25778 }),
25779 )
25780 .await;
25781
25782 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25783 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25784 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25785
25786 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25787 language_registry.add(rust_lang());
25788 let mut fake_servers = language_registry.register_fake_lsp(
25789 "Rust",
25790 FakeLspAdapter {
25791 capabilities: lsp::ServerCapabilities {
25792 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25793 ..lsp::ServerCapabilities::default()
25794 },
25795 name: "rust-analyzer",
25796 ..FakeLspAdapter::default()
25797 },
25798 );
25799 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25800 "Rust",
25801 FakeLspAdapter {
25802 capabilities: lsp::ServerCapabilities {
25803 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25804 ..lsp::ServerCapabilities::default()
25805 },
25806 name: "not-rust-analyzer",
25807 ..FakeLspAdapter::default()
25808 },
25809 );
25810
25811 let editor = workspace
25812 .update(cx, |workspace, window, cx| {
25813 workspace.open_abs_path(
25814 PathBuf::from(path!("/a/first.rs")),
25815 OpenOptions::default(),
25816 window,
25817 cx,
25818 )
25819 })
25820 .unwrap()
25821 .await
25822 .unwrap()
25823 .downcast::<Editor>()
25824 .unwrap();
25825 let fake_language_server = fake_servers.next().await.unwrap();
25826 let fake_language_server_without_capabilities =
25827 fake_servers_without_capabilities.next().await.unwrap();
25828 let requests_made = Arc::new(AtomicUsize::new(0));
25829 let closure_requests_made = Arc::clone(&requests_made);
25830 let mut color_request_handle = fake_language_server
25831 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25832 let requests_made = Arc::clone(&closure_requests_made);
25833 async move {
25834 assert_eq!(
25835 params.text_document.uri,
25836 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25837 );
25838 requests_made.fetch_add(1, atomic::Ordering::Release);
25839 Ok(vec![
25840 lsp::ColorInformation {
25841 range: lsp::Range {
25842 start: lsp::Position {
25843 line: 0,
25844 character: 0,
25845 },
25846 end: lsp::Position {
25847 line: 0,
25848 character: 1,
25849 },
25850 },
25851 color: lsp::Color {
25852 red: 0.33,
25853 green: 0.33,
25854 blue: 0.33,
25855 alpha: 0.33,
25856 },
25857 },
25858 lsp::ColorInformation {
25859 range: lsp::Range {
25860 start: lsp::Position {
25861 line: 0,
25862 character: 0,
25863 },
25864 end: lsp::Position {
25865 line: 0,
25866 character: 1,
25867 },
25868 },
25869 color: lsp::Color {
25870 red: 0.33,
25871 green: 0.33,
25872 blue: 0.33,
25873 alpha: 0.33,
25874 },
25875 },
25876 ])
25877 }
25878 });
25879
25880 let _handle = fake_language_server_without_capabilities
25881 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25882 panic!("Should not be called");
25883 });
25884 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
25885 color_request_handle.next().await.unwrap();
25886 cx.run_until_parked();
25887 assert_eq!(
25888 1,
25889 requests_made.load(atomic::Ordering::Acquire),
25890 "Should query for colors once per editor open"
25891 );
25892 editor.update_in(cx, |editor, _, cx| {
25893 assert_eq!(
25894 vec![expected_color],
25895 extract_color_inlays(editor, cx),
25896 "Should have an initial inlay"
25897 );
25898 });
25899
25900 // opening another file in a split should not influence the LSP query counter
25901 workspace
25902 .update(cx, |workspace, window, cx| {
25903 assert_eq!(
25904 workspace.panes().len(),
25905 1,
25906 "Should have one pane with one editor"
25907 );
25908 workspace.move_item_to_pane_in_direction(
25909 &MoveItemToPaneInDirection {
25910 direction: SplitDirection::Right,
25911 focus: false,
25912 clone: true,
25913 },
25914 window,
25915 cx,
25916 );
25917 })
25918 .unwrap();
25919 cx.run_until_parked();
25920 workspace
25921 .update(cx, |workspace, _, cx| {
25922 let panes = workspace.panes();
25923 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25924 for pane in panes {
25925 let editor = pane
25926 .read(cx)
25927 .active_item()
25928 .and_then(|item| item.downcast::<Editor>())
25929 .expect("Should have opened an editor in each split");
25930 let editor_file = editor
25931 .read(cx)
25932 .buffer()
25933 .read(cx)
25934 .as_singleton()
25935 .expect("test deals with singleton buffers")
25936 .read(cx)
25937 .file()
25938 .expect("test buffese should have a file")
25939 .path();
25940 assert_eq!(
25941 editor_file.as_ref(),
25942 rel_path("first.rs"),
25943 "Both editors should be opened for the same file"
25944 )
25945 }
25946 })
25947 .unwrap();
25948
25949 cx.executor().advance_clock(Duration::from_millis(500));
25950 let save = editor.update_in(cx, |editor, window, cx| {
25951 editor.move_to_end(&MoveToEnd, window, cx);
25952 editor.handle_input("dirty", window, cx);
25953 editor.save(
25954 SaveOptions {
25955 format: true,
25956 autosave: true,
25957 },
25958 project.clone(),
25959 window,
25960 cx,
25961 )
25962 });
25963 save.await.unwrap();
25964
25965 color_request_handle.next().await.unwrap();
25966 cx.run_until_parked();
25967 assert_eq!(
25968 2,
25969 requests_made.load(atomic::Ordering::Acquire),
25970 "Should query for colors once per save (deduplicated) and once per formatting after save"
25971 );
25972
25973 drop(editor);
25974 let close = workspace
25975 .update(cx, |workspace, window, cx| {
25976 workspace.active_pane().update(cx, |pane, cx| {
25977 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25978 })
25979 })
25980 .unwrap();
25981 close.await.unwrap();
25982 let close = workspace
25983 .update(cx, |workspace, window, cx| {
25984 workspace.active_pane().update(cx, |pane, cx| {
25985 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25986 })
25987 })
25988 .unwrap();
25989 close.await.unwrap();
25990 assert_eq!(
25991 2,
25992 requests_made.load(atomic::Ordering::Acquire),
25993 "After saving and closing all editors, no extra requests should be made"
25994 );
25995 workspace
25996 .update(cx, |workspace, _, cx| {
25997 assert!(
25998 workspace.active_item(cx).is_none(),
25999 "Should close all editors"
26000 )
26001 })
26002 .unwrap();
26003
26004 workspace
26005 .update(cx, |workspace, window, cx| {
26006 workspace.active_pane().update(cx, |pane, cx| {
26007 pane.navigate_backward(&workspace::GoBack, window, cx);
26008 })
26009 })
26010 .unwrap();
26011 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26012 cx.run_until_parked();
26013 let editor = workspace
26014 .update(cx, |workspace, _, cx| {
26015 workspace
26016 .active_item(cx)
26017 .expect("Should have reopened the editor again after navigating back")
26018 .downcast::<Editor>()
26019 .expect("Should be an editor")
26020 })
26021 .unwrap();
26022
26023 assert_eq!(
26024 2,
26025 requests_made.load(atomic::Ordering::Acquire),
26026 "Cache should be reused on buffer close and reopen"
26027 );
26028 editor.update(cx, |editor, cx| {
26029 assert_eq!(
26030 vec![expected_color],
26031 extract_color_inlays(editor, cx),
26032 "Should have an initial inlay"
26033 );
26034 });
26035
26036 drop(color_request_handle);
26037 let closure_requests_made = Arc::clone(&requests_made);
26038 let mut empty_color_request_handle = fake_language_server
26039 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26040 let requests_made = Arc::clone(&closure_requests_made);
26041 async move {
26042 assert_eq!(
26043 params.text_document.uri,
26044 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26045 );
26046 requests_made.fetch_add(1, atomic::Ordering::Release);
26047 Ok(Vec::new())
26048 }
26049 });
26050 let save = editor.update_in(cx, |editor, window, cx| {
26051 editor.move_to_end(&MoveToEnd, window, cx);
26052 editor.handle_input("dirty_again", window, cx);
26053 editor.save(
26054 SaveOptions {
26055 format: false,
26056 autosave: true,
26057 },
26058 project.clone(),
26059 window,
26060 cx,
26061 )
26062 });
26063 save.await.unwrap();
26064
26065 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26066 empty_color_request_handle.next().await.unwrap();
26067 cx.run_until_parked();
26068 assert_eq!(
26069 3,
26070 requests_made.load(atomic::Ordering::Acquire),
26071 "Should query for colors once per save only, as formatting was not requested"
26072 );
26073 editor.update(cx, |editor, cx| {
26074 assert_eq!(
26075 Vec::<Rgba>::new(),
26076 extract_color_inlays(editor, cx),
26077 "Should clear all colors when the server returns an empty response"
26078 );
26079 });
26080}
26081
26082#[gpui::test]
26083async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26084 init_test(cx, |_| {});
26085 let (editor, cx) = cx.add_window_view(Editor::single_line);
26086 editor.update_in(cx, |editor, window, cx| {
26087 editor.set_text("oops\n\nwow\n", window, cx)
26088 });
26089 cx.run_until_parked();
26090 editor.update(cx, |editor, cx| {
26091 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26092 });
26093 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
26094 cx.run_until_parked();
26095 editor.update(cx, |editor, cx| {
26096 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26097 });
26098}
26099
26100#[gpui::test]
26101async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26102 init_test(cx, |_| {});
26103
26104 cx.update(|cx| {
26105 register_project_item::<Editor>(cx);
26106 });
26107
26108 let fs = FakeFs::new(cx.executor());
26109 fs.insert_tree("/root1", json!({})).await;
26110 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26111 .await;
26112
26113 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26114 let (workspace, cx) =
26115 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26116
26117 let worktree_id = project.update(cx, |project, cx| {
26118 project.worktrees(cx).next().unwrap().read(cx).id()
26119 });
26120
26121 let handle = workspace
26122 .update_in(cx, |workspace, window, cx| {
26123 let project_path = (worktree_id, rel_path("one.pdf"));
26124 workspace.open_path(project_path, None, true, window, cx)
26125 })
26126 .await
26127 .unwrap();
26128
26129 assert_eq!(
26130 handle.to_any().entity_type(),
26131 TypeId::of::<InvalidBufferView>()
26132 );
26133}
26134
26135#[gpui::test]
26136async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26137 init_test(cx, |_| {});
26138
26139 let language = Arc::new(Language::new(
26140 LanguageConfig::default(),
26141 Some(tree_sitter_rust::LANGUAGE.into()),
26142 ));
26143
26144 // Test hierarchical sibling navigation
26145 let text = r#"
26146 fn outer() {
26147 if condition {
26148 let a = 1;
26149 }
26150 let b = 2;
26151 }
26152
26153 fn another() {
26154 let c = 3;
26155 }
26156 "#;
26157
26158 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
26159 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
26160 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
26161
26162 // Wait for parsing to complete
26163 editor
26164 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
26165 .await;
26166
26167 editor.update_in(cx, |editor, window, cx| {
26168 // Start by selecting "let a = 1;" inside the if block
26169 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26170 s.select_display_ranges([
26171 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
26172 ]);
26173 });
26174
26175 let initial_selection = editor.selections.display_ranges(cx);
26176 assert_eq!(initial_selection.len(), 1, "Should have one selection");
26177
26178 // Test select next sibling - should move up levels to find the next sibling
26179 // Since "let a = 1;" has no siblings in the if block, it should move up
26180 // to find "let b = 2;" which is a sibling of the if block
26181 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26182 let next_selection = editor.selections.display_ranges(cx);
26183
26184 // Should have a selection and it should be different from the initial
26185 assert_eq!(
26186 next_selection.len(),
26187 1,
26188 "Should have one selection after next"
26189 );
26190 assert_ne!(
26191 next_selection[0], initial_selection[0],
26192 "Next sibling selection should be different"
26193 );
26194
26195 // Test hierarchical navigation by going to the end of the current function
26196 // and trying to navigate to the next function
26197 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26198 s.select_display_ranges([
26199 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26200 ]);
26201 });
26202
26203 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26204 let function_next_selection = editor.selections.display_ranges(cx);
26205
26206 // Should move to the next function
26207 assert_eq!(
26208 function_next_selection.len(),
26209 1,
26210 "Should have one selection after function next"
26211 );
26212
26213 // Test select previous sibling navigation
26214 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26215 let prev_selection = editor.selections.display_ranges(cx);
26216
26217 // Should have a selection and it should be different
26218 assert_eq!(
26219 prev_selection.len(),
26220 1,
26221 "Should have one selection after prev"
26222 );
26223 assert_ne!(
26224 prev_selection[0], function_next_selection[0],
26225 "Previous sibling selection should be different from next"
26226 );
26227 });
26228}
26229
26230#[gpui::test]
26231async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26232 init_test(cx, |_| {});
26233
26234 let mut cx = EditorTestContext::new(cx).await;
26235 cx.set_state(
26236 "let ˇvariable = 42;
26237let another = variable + 1;
26238let result = variable * 2;",
26239 );
26240
26241 // Set up document highlights manually (simulating LSP response)
26242 cx.update_editor(|editor, _window, cx| {
26243 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26244
26245 // Create highlights for "variable" occurrences
26246 let highlight_ranges = [
26247 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26248 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26249 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26250 ];
26251
26252 let anchor_ranges: Vec<_> = highlight_ranges
26253 .iter()
26254 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26255 .collect();
26256
26257 editor.highlight_background::<DocumentHighlightRead>(
26258 &anchor_ranges,
26259 |theme| theme.colors().editor_document_highlight_read_background,
26260 cx,
26261 );
26262 });
26263
26264 // Go to next highlight - should move to second "variable"
26265 cx.update_editor(|editor, window, cx| {
26266 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26267 });
26268 cx.assert_editor_state(
26269 "let variable = 42;
26270let another = ˇvariable + 1;
26271let result = variable * 2;",
26272 );
26273
26274 // Go to next highlight - should move to third "variable"
26275 cx.update_editor(|editor, window, cx| {
26276 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26277 });
26278 cx.assert_editor_state(
26279 "let variable = 42;
26280let another = variable + 1;
26281let result = ˇvariable * 2;",
26282 );
26283
26284 // Go to next highlight - should stay at third "variable" (no wrap-around)
26285 cx.update_editor(|editor, window, cx| {
26286 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26287 });
26288 cx.assert_editor_state(
26289 "let variable = 42;
26290let another = variable + 1;
26291let result = ˇvariable * 2;",
26292 );
26293
26294 // Now test going backwards from third position
26295 cx.update_editor(|editor, window, cx| {
26296 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26297 });
26298 cx.assert_editor_state(
26299 "let variable = 42;
26300let another = ˇvariable + 1;
26301let result = variable * 2;",
26302 );
26303
26304 // Go to previous highlight - should move to first "variable"
26305 cx.update_editor(|editor, window, cx| {
26306 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26307 });
26308 cx.assert_editor_state(
26309 "let ˇvariable = 42;
26310let another = variable + 1;
26311let result = variable * 2;",
26312 );
26313
26314 // Go to previous highlight - should stay on first "variable"
26315 cx.update_editor(|editor, window, cx| {
26316 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26317 });
26318 cx.assert_editor_state(
26319 "let ˇvariable = 42;
26320let another = variable + 1;
26321let result = variable * 2;",
26322 );
26323}
26324
26325#[gpui::test]
26326async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26327 cx: &mut gpui::TestAppContext,
26328) {
26329 init_test(cx, |_| {});
26330
26331 let url = "https://zed.dev";
26332
26333 let markdown_language = Arc::new(Language::new(
26334 LanguageConfig {
26335 name: "Markdown".into(),
26336 ..LanguageConfig::default()
26337 },
26338 None,
26339 ));
26340
26341 let mut cx = EditorTestContext::new(cx).await;
26342 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26343 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26344
26345 cx.update_editor(|editor, window, cx| {
26346 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26347 editor.paste(&Paste, window, cx);
26348 });
26349
26350 cx.assert_editor_state(&format!(
26351 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26352 ));
26353}
26354
26355#[gpui::test]
26356async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26357 cx: &mut gpui::TestAppContext,
26358) {
26359 init_test(cx, |_| {});
26360
26361 let url = "https://zed.dev";
26362
26363 let markdown_language = Arc::new(Language::new(
26364 LanguageConfig {
26365 name: "Markdown".into(),
26366 ..LanguageConfig::default()
26367 },
26368 None,
26369 ));
26370
26371 let mut cx = EditorTestContext::new(cx).await;
26372 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26373 cx.set_state(&format!(
26374 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26375 ));
26376
26377 cx.update_editor(|editor, window, cx| {
26378 editor.copy(&Copy, window, cx);
26379 });
26380
26381 cx.set_state(&format!(
26382 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26383 ));
26384
26385 cx.update_editor(|editor, window, cx| {
26386 editor.paste(&Paste, window, cx);
26387 });
26388
26389 cx.assert_editor_state(&format!(
26390 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26391 ));
26392}
26393
26394#[gpui::test]
26395async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26396 cx: &mut gpui::TestAppContext,
26397) {
26398 init_test(cx, |_| {});
26399
26400 let url = "https://zed.dev";
26401
26402 let markdown_language = Arc::new(Language::new(
26403 LanguageConfig {
26404 name: "Markdown".into(),
26405 ..LanguageConfig::default()
26406 },
26407 None,
26408 ));
26409
26410 let mut cx = EditorTestContext::new(cx).await;
26411 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26412 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26413
26414 cx.update_editor(|editor, window, cx| {
26415 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26416 editor.paste(&Paste, window, cx);
26417 });
26418
26419 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26420}
26421
26422#[gpui::test]
26423async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26424 cx: &mut gpui::TestAppContext,
26425) {
26426 init_test(cx, |_| {});
26427
26428 let text = "Awesome";
26429
26430 let markdown_language = Arc::new(Language::new(
26431 LanguageConfig {
26432 name: "Markdown".into(),
26433 ..LanguageConfig::default()
26434 },
26435 None,
26436 ));
26437
26438 let mut cx = EditorTestContext::new(cx).await;
26439 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26440 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26441
26442 cx.update_editor(|editor, window, cx| {
26443 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26444 editor.paste(&Paste, window, cx);
26445 });
26446
26447 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26448}
26449
26450#[gpui::test]
26451async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26452 cx: &mut gpui::TestAppContext,
26453) {
26454 init_test(cx, |_| {});
26455
26456 let url = "https://zed.dev";
26457
26458 let markdown_language = Arc::new(Language::new(
26459 LanguageConfig {
26460 name: "Rust".into(),
26461 ..LanguageConfig::default()
26462 },
26463 None,
26464 ));
26465
26466 let mut cx = EditorTestContext::new(cx).await;
26467 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26468 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26469
26470 cx.update_editor(|editor, window, cx| {
26471 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26472 editor.paste(&Paste, window, cx);
26473 });
26474
26475 cx.assert_editor_state(&format!(
26476 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26477 ));
26478}
26479
26480#[gpui::test]
26481async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26482 cx: &mut TestAppContext,
26483) {
26484 init_test(cx, |_| {});
26485
26486 let url = "https://zed.dev";
26487
26488 let markdown_language = Arc::new(Language::new(
26489 LanguageConfig {
26490 name: "Markdown".into(),
26491 ..LanguageConfig::default()
26492 },
26493 None,
26494 ));
26495
26496 let (editor, cx) = cx.add_window_view(|window, cx| {
26497 let multi_buffer = MultiBuffer::build_multi(
26498 [
26499 ("this will embed -> link", vec![Point::row_range(0..1)]),
26500 ("this will replace -> link", vec![Point::row_range(0..1)]),
26501 ],
26502 cx,
26503 );
26504 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26505 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26506 s.select_ranges(vec![
26507 Point::new(0, 19)..Point::new(0, 23),
26508 Point::new(1, 21)..Point::new(1, 25),
26509 ])
26510 });
26511 let first_buffer_id = multi_buffer
26512 .read(cx)
26513 .excerpt_buffer_ids()
26514 .into_iter()
26515 .next()
26516 .unwrap();
26517 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26518 first_buffer.update(cx, |buffer, cx| {
26519 buffer.set_language(Some(markdown_language.clone()), cx);
26520 });
26521
26522 editor
26523 });
26524 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26525
26526 cx.update_editor(|editor, window, cx| {
26527 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26528 editor.paste(&Paste, window, cx);
26529 });
26530
26531 cx.assert_editor_state(&format!(
26532 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26533 ));
26534}
26535
26536#[gpui::test]
26537async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26538 init_test(cx, |_| {});
26539
26540 let fs = FakeFs::new(cx.executor());
26541 fs.insert_tree(
26542 path!("/project"),
26543 json!({
26544 "first.rs": "# First Document\nSome content here.",
26545 "second.rs": "Plain text content for second file.",
26546 }),
26547 )
26548 .await;
26549
26550 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26551 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26552 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26553
26554 let language = rust_lang();
26555 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26556 language_registry.add(language.clone());
26557 let mut fake_servers = language_registry.register_fake_lsp(
26558 "Rust",
26559 FakeLspAdapter {
26560 ..FakeLspAdapter::default()
26561 },
26562 );
26563
26564 let buffer1 = project
26565 .update(cx, |project, cx| {
26566 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26567 })
26568 .await
26569 .unwrap();
26570 let buffer2 = project
26571 .update(cx, |project, cx| {
26572 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26573 })
26574 .await
26575 .unwrap();
26576
26577 let multi_buffer = cx.new(|cx| {
26578 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26579 multi_buffer.set_excerpts_for_path(
26580 PathKey::for_buffer(&buffer1, cx),
26581 buffer1.clone(),
26582 [Point::zero()..buffer1.read(cx).max_point()],
26583 3,
26584 cx,
26585 );
26586 multi_buffer.set_excerpts_for_path(
26587 PathKey::for_buffer(&buffer2, cx),
26588 buffer2.clone(),
26589 [Point::zero()..buffer1.read(cx).max_point()],
26590 3,
26591 cx,
26592 );
26593 multi_buffer
26594 });
26595
26596 let (editor, cx) = cx.add_window_view(|window, cx| {
26597 Editor::new(
26598 EditorMode::full(),
26599 multi_buffer,
26600 Some(project.clone()),
26601 window,
26602 cx,
26603 )
26604 });
26605
26606 let fake_language_server = fake_servers.next().await.unwrap();
26607
26608 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26609
26610 let save = editor.update_in(cx, |editor, window, cx| {
26611 assert!(editor.is_dirty(cx));
26612
26613 editor.save(
26614 SaveOptions {
26615 format: true,
26616 autosave: true,
26617 },
26618 project,
26619 window,
26620 cx,
26621 )
26622 });
26623 let (start_edit_tx, start_edit_rx) = oneshot::channel();
26624 let (done_edit_tx, done_edit_rx) = oneshot::channel();
26625 let mut done_edit_rx = Some(done_edit_rx);
26626 let mut start_edit_tx = Some(start_edit_tx);
26627
26628 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
26629 start_edit_tx.take().unwrap().send(()).unwrap();
26630 let done_edit_rx = done_edit_rx.take().unwrap();
26631 async move {
26632 done_edit_rx.await.unwrap();
26633 Ok(None)
26634 }
26635 });
26636
26637 start_edit_rx.await.unwrap();
26638 buffer2
26639 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
26640 .unwrap();
26641
26642 done_edit_tx.send(()).unwrap();
26643
26644 save.await.unwrap();
26645 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
26646}
26647
26648#[track_caller]
26649fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26650 editor
26651 .all_inlays(cx)
26652 .into_iter()
26653 .filter_map(|inlay| inlay.get_color())
26654 .map(Rgba::from)
26655 .collect()
26656}
26657
26658#[gpui::test]
26659fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
26660 init_test(cx, |_| {});
26661
26662 let editor = cx.add_window(|window, cx| {
26663 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
26664 build_editor(buffer, window, cx)
26665 });
26666
26667 editor
26668 .update(cx, |editor, window, cx| {
26669 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26670 s.select_display_ranges([
26671 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
26672 ])
26673 });
26674
26675 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
26676
26677 assert_eq!(
26678 editor.display_text(cx),
26679 "line1\nline2\nline2",
26680 "Duplicating last line upward should create duplicate above, not on same line"
26681 );
26682
26683 assert_eq!(
26684 editor.selections.display_ranges(cx),
26685 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)],
26686 "Selection should remain on the original line"
26687 );
26688 })
26689 .unwrap();
26690}
26691
26692#[gpui::test]
26693async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
26694 init_test(cx, |_| {});
26695
26696 let mut cx = EditorTestContext::new(cx).await;
26697
26698 cx.set_state("line1\nline2ˇ");
26699
26700 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
26701
26702 let clipboard_text = cx
26703 .read_from_clipboard()
26704 .and_then(|item| item.text().as_deref().map(str::to_string));
26705
26706 assert_eq!(
26707 clipboard_text,
26708 Some("line2\n".to_string()),
26709 "Copying a line without trailing newline should include a newline"
26710 );
26711
26712 cx.set_state("line1\nˇ");
26713
26714 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
26715
26716 cx.assert_editor_state("line1\nline2\nˇ");
26717}