1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use collections::HashMap;
17use futures::{StreamExt, channel::oneshot};
18use gpui::{
19 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
20 VisualTestContext, WindowBounds, WindowOptions, div,
21};
22use indoc::indoc;
23use language::{
24 BracketPairConfig,
25 Capability::ReadWrite,
26 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
27 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
28 language_settings::{
29 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
30 },
31 tree_sitter_python,
32};
33use language_settings::Formatter;
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::LspSettings,
42};
43use serde_json::{self, json};
44use settings::{
45 AllLanguageSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring,
46 ProjectSettingsContent,
47};
48use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
49use std::{
50 iter,
51 sync::atomic::{self, AtomicUsize},
52};
53use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
54use text::ToPoint as _;
55use unindent::Unindent;
56use util::{
57 assert_set_eq, path,
58 rel_path::rel_path,
59 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
60 uri,
61};
62use workspace::{
63 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
64 OpenOptions, ViewId,
65 invalid_buffer_view::InvalidBufferView,
66 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
67 register_project_item,
68};
69
70#[gpui::test]
71fn test_edit_events(cx: &mut TestAppContext) {
72 init_test(cx, |_| {});
73
74 let buffer = cx.new(|cx| {
75 let mut buffer = language::Buffer::local("123456", cx);
76 buffer.set_group_interval(Duration::from_secs(1));
77 buffer
78 });
79
80 let events = Rc::new(RefCell::new(Vec::new()));
81 let editor1 = cx.add_window({
82 let events = events.clone();
83 |window, cx| {
84 let entity = cx.entity();
85 cx.subscribe_in(
86 &entity,
87 window,
88 move |_, _, event: &EditorEvent, _, _| match event {
89 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
90 EditorEvent::BufferEdited => {
91 events.borrow_mut().push(("editor1", "buffer edited"))
92 }
93 _ => {}
94 },
95 )
96 .detach();
97 Editor::for_buffer(buffer.clone(), None, window, cx)
98 }
99 });
100
101 let editor2 = cx.add_window({
102 let events = events.clone();
103 |window, cx| {
104 cx.subscribe_in(
105 &cx.entity(),
106 window,
107 move |_, _, event: &EditorEvent, _, _| match event {
108 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
109 EditorEvent::BufferEdited => {
110 events.borrow_mut().push(("editor2", "buffer edited"))
111 }
112 _ => {}
113 },
114 )
115 .detach();
116 Editor::for_buffer(buffer.clone(), None, window, cx)
117 }
118 });
119
120 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
121
122 // Mutating editor 1 will emit an `Edited` event only for that editor.
123 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
124 assert_eq!(
125 mem::take(&mut *events.borrow_mut()),
126 [
127 ("editor1", "edited"),
128 ("editor1", "buffer edited"),
129 ("editor2", "buffer edited"),
130 ]
131 );
132
133 // Mutating editor 2 will emit an `Edited` event only for that editor.
134 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
135 assert_eq!(
136 mem::take(&mut *events.borrow_mut()),
137 [
138 ("editor2", "edited"),
139 ("editor1", "buffer edited"),
140 ("editor2", "buffer edited"),
141 ]
142 );
143
144 // Undoing on editor 1 will emit an `Edited` event only for that editor.
145 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
146 assert_eq!(
147 mem::take(&mut *events.borrow_mut()),
148 [
149 ("editor1", "edited"),
150 ("editor1", "buffer edited"),
151 ("editor2", "buffer edited"),
152 ]
153 );
154
155 // Redoing on editor 1 will emit an `Edited` event only for that editor.
156 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
157 assert_eq!(
158 mem::take(&mut *events.borrow_mut()),
159 [
160 ("editor1", "edited"),
161 ("editor1", "buffer edited"),
162 ("editor2", "buffer edited"),
163 ]
164 );
165
166 // Undoing on editor 2 will emit an `Edited` event only for that editor.
167 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
168 assert_eq!(
169 mem::take(&mut *events.borrow_mut()),
170 [
171 ("editor2", "edited"),
172 ("editor1", "buffer edited"),
173 ("editor2", "buffer edited"),
174 ]
175 );
176
177 // Redoing on editor 2 will emit an `Edited` event only for that editor.
178 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
179 assert_eq!(
180 mem::take(&mut *events.borrow_mut()),
181 [
182 ("editor2", "edited"),
183 ("editor1", "buffer edited"),
184 ("editor2", "buffer edited"),
185 ]
186 );
187
188 // No event is emitted when the mutation is a no-op.
189 _ = editor2.update(cx, |editor, window, cx| {
190 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
191 s.select_ranges([0..0])
192 });
193
194 editor.backspace(&Backspace, window, cx);
195 });
196 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
197}
198
199#[gpui::test]
200fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
201 init_test(cx, |_| {});
202
203 let mut now = Instant::now();
204 let group_interval = Duration::from_millis(1);
205 let buffer = cx.new(|cx| {
206 let mut buf = language::Buffer::local("123456", cx);
207 buf.set_group_interval(group_interval);
208 buf
209 });
210 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
211 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
212
213 _ = editor.update(cx, |editor, window, cx| {
214 editor.start_transaction_at(now, window, cx);
215 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
216 s.select_ranges([2..4])
217 });
218
219 editor.insert("cd", window, cx);
220 editor.end_transaction_at(now, cx);
221 assert_eq!(editor.text(cx), "12cd56");
222 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
223
224 editor.start_transaction_at(now, window, cx);
225 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
226 s.select_ranges([4..5])
227 });
228 editor.insert("e", window, cx);
229 editor.end_transaction_at(now, cx);
230 assert_eq!(editor.text(cx), "12cde6");
231 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
232
233 now += group_interval + Duration::from_millis(1);
234 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
235 s.select_ranges([2..2])
236 });
237
238 // Simulate an edit in another editor
239 buffer.update(cx, |buffer, cx| {
240 buffer.start_transaction_at(now, cx);
241 buffer.edit([(0..1, "a")], None, cx);
242 buffer.edit([(1..1, "b")], None, cx);
243 buffer.end_transaction_at(now, cx);
244 });
245
246 assert_eq!(editor.text(cx), "ab2cde6");
247 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
248
249 // Last transaction happened past the group interval in a different editor.
250 // Undo it individually and don't restore selections.
251 editor.undo(&Undo, window, cx);
252 assert_eq!(editor.text(cx), "12cde6");
253 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
254
255 // First two transactions happened within the group interval in this editor.
256 // Undo them together and restore selections.
257 editor.undo(&Undo, window, cx);
258 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
259 assert_eq!(editor.text(cx), "123456");
260 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
261
262 // Redo the first two transactions together.
263 editor.redo(&Redo, window, cx);
264 assert_eq!(editor.text(cx), "12cde6");
265 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
266
267 // Redo the last transaction on its own.
268 editor.redo(&Redo, window, cx);
269 assert_eq!(editor.text(cx), "ab2cde6");
270 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
271
272 // Test empty transactions.
273 editor.start_transaction_at(now, window, cx);
274 editor.end_transaction_at(now, cx);
275 editor.undo(&Undo, window, cx);
276 assert_eq!(editor.text(cx), "12cde6");
277 });
278}
279
280#[gpui::test]
281fn test_ime_composition(cx: &mut TestAppContext) {
282 init_test(cx, |_| {});
283
284 let buffer = cx.new(|cx| {
285 let mut buffer = language::Buffer::local("abcde", cx);
286 // Ensure automatic grouping doesn't occur.
287 buffer.set_group_interval(Duration::ZERO);
288 buffer
289 });
290
291 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
292 cx.add_window(|window, cx| {
293 let mut editor = build_editor(buffer.clone(), window, cx);
294
295 // Start a new IME composition.
296 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
297 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
298 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
299 assert_eq!(editor.text(cx), "äbcde");
300 assert_eq!(
301 editor.marked_text_ranges(cx),
302 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
303 );
304
305 // Finalize IME composition.
306 editor.replace_text_in_range(None, "ā", window, cx);
307 assert_eq!(editor.text(cx), "ābcde");
308 assert_eq!(editor.marked_text_ranges(cx), None);
309
310 // IME composition edits are grouped and are undone/redone at once.
311 editor.undo(&Default::default(), window, cx);
312 assert_eq!(editor.text(cx), "abcde");
313 assert_eq!(editor.marked_text_ranges(cx), None);
314 editor.redo(&Default::default(), window, cx);
315 assert_eq!(editor.text(cx), "ābcde");
316 assert_eq!(editor.marked_text_ranges(cx), None);
317
318 // Start a new IME composition.
319 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
320 assert_eq!(
321 editor.marked_text_ranges(cx),
322 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
323 );
324
325 // Undoing during an IME composition cancels it.
326 editor.undo(&Default::default(), window, cx);
327 assert_eq!(editor.text(cx), "ābcde");
328 assert_eq!(editor.marked_text_ranges(cx), None);
329
330 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
331 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
332 assert_eq!(editor.text(cx), "ābcdè");
333 assert_eq!(
334 editor.marked_text_ranges(cx),
335 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
336 );
337
338 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
339 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
340 assert_eq!(editor.text(cx), "ābcdę");
341 assert_eq!(editor.marked_text_ranges(cx), None);
342
343 // Start a new IME composition with multiple cursors.
344 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
345 s.select_ranges([
346 OffsetUtf16(1)..OffsetUtf16(1),
347 OffsetUtf16(3)..OffsetUtf16(3),
348 OffsetUtf16(5)..OffsetUtf16(5),
349 ])
350 });
351 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
352 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
353 assert_eq!(
354 editor.marked_text_ranges(cx),
355 Some(vec![
356 OffsetUtf16(0)..OffsetUtf16(3),
357 OffsetUtf16(4)..OffsetUtf16(7),
358 OffsetUtf16(8)..OffsetUtf16(11)
359 ])
360 );
361
362 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
363 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
364 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
365 assert_eq!(
366 editor.marked_text_ranges(cx),
367 Some(vec![
368 OffsetUtf16(1)..OffsetUtf16(2),
369 OffsetUtf16(5)..OffsetUtf16(6),
370 OffsetUtf16(9)..OffsetUtf16(10)
371 ])
372 );
373
374 // Finalize IME composition with multiple cursors.
375 editor.replace_text_in_range(Some(9..10), "2", window, cx);
376 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
377 assert_eq!(editor.marked_text_ranges(cx), None);
378
379 editor
380 });
381}
382
383#[gpui::test]
384fn test_selection_with_mouse(cx: &mut TestAppContext) {
385 init_test(cx, |_| {});
386
387 let editor = cx.add_window(|window, cx| {
388 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
389 build_editor(buffer, window, cx)
390 });
391
392 _ = editor.update(cx, |editor, window, cx| {
393 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
394 });
395 assert_eq!(
396 editor
397 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
398 .unwrap(),
399 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
400 );
401
402 _ = editor.update(cx, |editor, window, cx| {
403 editor.update_selection(
404 DisplayPoint::new(DisplayRow(3), 3),
405 0,
406 gpui::Point::<f32>::default(),
407 window,
408 cx,
409 );
410 });
411
412 assert_eq!(
413 editor
414 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
415 .unwrap(),
416 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
417 );
418
419 _ = editor.update(cx, |editor, window, cx| {
420 editor.update_selection(
421 DisplayPoint::new(DisplayRow(1), 1),
422 0,
423 gpui::Point::<f32>::default(),
424 window,
425 cx,
426 );
427 });
428
429 assert_eq!(
430 editor
431 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
432 .unwrap(),
433 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
434 );
435
436 _ = editor.update(cx, |editor, window, cx| {
437 editor.end_selection(window, cx);
438 editor.update_selection(
439 DisplayPoint::new(DisplayRow(3), 3),
440 0,
441 gpui::Point::<f32>::default(),
442 window,
443 cx,
444 );
445 });
446
447 assert_eq!(
448 editor
449 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
450 .unwrap(),
451 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
452 );
453
454 _ = editor.update(cx, |editor, window, cx| {
455 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
456 editor.update_selection(
457 DisplayPoint::new(DisplayRow(0), 0),
458 0,
459 gpui::Point::<f32>::default(),
460 window,
461 cx,
462 );
463 });
464
465 assert_eq!(
466 editor
467 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
468 .unwrap(),
469 [
470 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
471 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
472 ]
473 );
474
475 _ = editor.update(cx, |editor, window, cx| {
476 editor.end_selection(window, cx);
477 });
478
479 assert_eq!(
480 editor
481 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
482 .unwrap(),
483 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
484 );
485}
486
487#[gpui::test]
488fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
489 init_test(cx, |_| {});
490
491 let editor = cx.add_window(|window, cx| {
492 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
493 build_editor(buffer, window, cx)
494 });
495
496 _ = editor.update(cx, |editor, window, cx| {
497 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
498 });
499
500 _ = editor.update(cx, |editor, window, cx| {
501 editor.end_selection(window, cx);
502 });
503
504 _ = editor.update(cx, |editor, window, cx| {
505 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
506 });
507
508 _ = editor.update(cx, |editor, window, cx| {
509 editor.end_selection(window, cx);
510 });
511
512 assert_eq!(
513 editor
514 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
515 .unwrap(),
516 [
517 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
518 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
519 ]
520 );
521
522 _ = editor.update(cx, |editor, window, cx| {
523 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
524 });
525
526 _ = editor.update(cx, |editor, window, cx| {
527 editor.end_selection(window, cx);
528 });
529
530 assert_eq!(
531 editor
532 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
533 .unwrap(),
534 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
535 );
536}
537
538#[gpui::test]
539fn test_canceling_pending_selection(cx: &mut TestAppContext) {
540 init_test(cx, |_| {});
541
542 let editor = cx.add_window(|window, cx| {
543 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
544 build_editor(buffer, window, cx)
545 });
546
547 _ = editor.update(cx, |editor, window, cx| {
548 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
549 assert_eq!(
550 editor.selections.display_ranges(cx),
551 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
552 );
553 });
554
555 _ = editor.update(cx, |editor, window, cx| {
556 editor.update_selection(
557 DisplayPoint::new(DisplayRow(3), 3),
558 0,
559 gpui::Point::<f32>::default(),
560 window,
561 cx,
562 );
563 assert_eq!(
564 editor.selections.display_ranges(cx),
565 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
566 );
567 });
568
569 _ = editor.update(cx, |editor, window, cx| {
570 editor.cancel(&Cancel, window, cx);
571 editor.update_selection(
572 DisplayPoint::new(DisplayRow(1), 1),
573 0,
574 gpui::Point::<f32>::default(),
575 window,
576 cx,
577 );
578 assert_eq!(
579 editor.selections.display_ranges(cx),
580 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
581 );
582 });
583}
584
585#[gpui::test]
586fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
587 init_test(cx, |_| {});
588
589 let editor = cx.add_window(|window, cx| {
590 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
591 build_editor(buffer, window, cx)
592 });
593
594 _ = editor.update(cx, |editor, window, cx| {
595 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
596 assert_eq!(
597 editor.selections.display_ranges(cx),
598 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
599 );
600
601 editor.move_down(&Default::default(), window, cx);
602 assert_eq!(
603 editor.selections.display_ranges(cx),
604 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
605 );
606
607 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
608 assert_eq!(
609 editor.selections.display_ranges(cx),
610 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
611 );
612
613 editor.move_up(&Default::default(), window, cx);
614 assert_eq!(
615 editor.selections.display_ranges(cx),
616 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
617 );
618 });
619}
620
621#[gpui::test]
622fn test_extending_selection(cx: &mut TestAppContext) {
623 init_test(cx, |_| {});
624
625 let editor = cx.add_window(|window, cx| {
626 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
627 build_editor(buffer, window, cx)
628 });
629
630 _ = editor.update(cx, |editor, window, cx| {
631 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
632 editor.end_selection(window, cx);
633 assert_eq!(
634 editor.selections.display_ranges(cx),
635 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
636 );
637
638 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
639 editor.end_selection(window, cx);
640 assert_eq!(
641 editor.selections.display_ranges(cx),
642 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
643 );
644
645 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
646 editor.end_selection(window, cx);
647 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
648 assert_eq!(
649 editor.selections.display_ranges(cx),
650 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
651 );
652
653 editor.update_selection(
654 DisplayPoint::new(DisplayRow(0), 1),
655 0,
656 gpui::Point::<f32>::default(),
657 window,
658 cx,
659 );
660 editor.end_selection(window, cx);
661 assert_eq!(
662 editor.selections.display_ranges(cx),
663 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
664 );
665
666 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
667 editor.end_selection(window, cx);
668 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
669 editor.end_selection(window, cx);
670 assert_eq!(
671 editor.selections.display_ranges(cx),
672 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
673 );
674
675 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
676 assert_eq!(
677 editor.selections.display_ranges(cx),
678 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
679 );
680
681 editor.update_selection(
682 DisplayPoint::new(DisplayRow(0), 6),
683 0,
684 gpui::Point::<f32>::default(),
685 window,
686 cx,
687 );
688 assert_eq!(
689 editor.selections.display_ranges(cx),
690 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
691 );
692
693 editor.update_selection(
694 DisplayPoint::new(DisplayRow(0), 1),
695 0,
696 gpui::Point::<f32>::default(),
697 window,
698 cx,
699 );
700 editor.end_selection(window, cx);
701 assert_eq!(
702 editor.selections.display_ranges(cx),
703 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
704 );
705 });
706}
707
708#[gpui::test]
709fn test_clone(cx: &mut TestAppContext) {
710 init_test(cx, |_| {});
711
712 let (text, selection_ranges) = marked_text_ranges(
713 indoc! {"
714 one
715 two
716 threeˇ
717 four
718 fiveˇ
719 "},
720 true,
721 );
722
723 let editor = cx.add_window(|window, cx| {
724 let buffer = MultiBuffer::build_simple(&text, cx);
725 build_editor(buffer, window, cx)
726 });
727
728 _ = editor.update(cx, |editor, window, cx| {
729 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
730 s.select_ranges(selection_ranges.clone())
731 });
732 editor.fold_creases(
733 vec![
734 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
735 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
736 ],
737 true,
738 window,
739 cx,
740 );
741 });
742
743 let cloned_editor = editor
744 .update(cx, |editor, _, cx| {
745 cx.open_window(Default::default(), |window, cx| {
746 cx.new(|cx| editor.clone(window, cx))
747 })
748 })
749 .unwrap()
750 .unwrap();
751
752 let snapshot = editor
753 .update(cx, |e, window, cx| e.snapshot(window, cx))
754 .unwrap();
755 let cloned_snapshot = cloned_editor
756 .update(cx, |e, window, cx| e.snapshot(window, cx))
757 .unwrap();
758
759 assert_eq!(
760 cloned_editor
761 .update(cx, |e, _, cx| e.display_text(cx))
762 .unwrap(),
763 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
764 );
765 assert_eq!(
766 cloned_snapshot
767 .folds_in_range(0..text.len())
768 .collect::<Vec<_>>(),
769 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
770 );
771 assert_set_eq!(
772 cloned_editor
773 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
774 .unwrap(),
775 editor
776 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
777 .unwrap()
778 );
779 assert_set_eq!(
780 cloned_editor
781 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
782 .unwrap(),
783 editor
784 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
785 .unwrap()
786 );
787}
788
789#[gpui::test]
790async fn test_navigation_history(cx: &mut TestAppContext) {
791 init_test(cx, |_| {});
792
793 use workspace::item::Item;
794
795 let fs = FakeFs::new(cx.executor());
796 let project = Project::test(fs, [], cx).await;
797 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
798 let pane = workspace
799 .update(cx, |workspace, _, _| workspace.active_pane().clone())
800 .unwrap();
801
802 _ = workspace.update(cx, |_v, window, cx| {
803 cx.new(|cx| {
804 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
805 let mut editor = build_editor(buffer, window, cx);
806 let handle = cx.entity();
807 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
808
809 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
810 editor.nav_history.as_mut().unwrap().pop_backward(cx)
811 }
812
813 // Move the cursor a small distance.
814 // Nothing is added to the navigation history.
815 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
816 s.select_display_ranges([
817 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
818 ])
819 });
820 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
821 s.select_display_ranges([
822 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
823 ])
824 });
825 assert!(pop_history(&mut editor, cx).is_none());
826
827 // Move the cursor a large distance.
828 // The history can jump back to the previous position.
829 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
830 s.select_display_ranges([
831 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
832 ])
833 });
834 let nav_entry = pop_history(&mut editor, cx).unwrap();
835 editor.navigate(nav_entry.data.unwrap(), window, cx);
836 assert_eq!(nav_entry.item.id(), cx.entity_id());
837 assert_eq!(
838 editor.selections.display_ranges(cx),
839 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
840 );
841 assert!(pop_history(&mut editor, cx).is_none());
842
843 // Move the cursor a small distance via the mouse.
844 // Nothing is added to the navigation history.
845 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
846 editor.end_selection(window, cx);
847 assert_eq!(
848 editor.selections.display_ranges(cx),
849 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
850 );
851 assert!(pop_history(&mut editor, cx).is_none());
852
853 // Move the cursor a large distance via the mouse.
854 // The history can jump back to the previous position.
855 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
856 editor.end_selection(window, cx);
857 assert_eq!(
858 editor.selections.display_ranges(cx),
859 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
860 );
861 let nav_entry = pop_history(&mut editor, cx).unwrap();
862 editor.navigate(nav_entry.data.unwrap(), window, cx);
863 assert_eq!(nav_entry.item.id(), cx.entity_id());
864 assert_eq!(
865 editor.selections.display_ranges(cx),
866 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
867 );
868 assert!(pop_history(&mut editor, cx).is_none());
869
870 // Set scroll position to check later
871 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
872 let original_scroll_position = editor.scroll_manager.anchor();
873
874 // Jump to the end of the document and adjust scroll
875 editor.move_to_end(&MoveToEnd, window, cx);
876 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
877 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
878
879 let nav_entry = pop_history(&mut editor, cx).unwrap();
880 editor.navigate(nav_entry.data.unwrap(), window, cx);
881 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
882
883 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
884 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
885 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
886 let invalid_point = Point::new(9999, 0);
887 editor.navigate(
888 Box::new(NavigationData {
889 cursor_anchor: invalid_anchor,
890 cursor_position: invalid_point,
891 scroll_anchor: ScrollAnchor {
892 anchor: invalid_anchor,
893 offset: Default::default(),
894 },
895 scroll_top_row: invalid_point.row,
896 }),
897 window,
898 cx,
899 );
900 assert_eq!(
901 editor.selections.display_ranges(cx),
902 &[editor.max_point(cx)..editor.max_point(cx)]
903 );
904 assert_eq!(
905 editor.scroll_position(cx),
906 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
907 );
908
909 editor
910 })
911 });
912}
913
914#[gpui::test]
915fn test_cancel(cx: &mut TestAppContext) {
916 init_test(cx, |_| {});
917
918 let editor = cx.add_window(|window, cx| {
919 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
920 build_editor(buffer, window, cx)
921 });
922
923 _ = editor.update(cx, |editor, window, cx| {
924 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
925 editor.update_selection(
926 DisplayPoint::new(DisplayRow(1), 1),
927 0,
928 gpui::Point::<f32>::default(),
929 window,
930 cx,
931 );
932 editor.end_selection(window, cx);
933
934 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
935 editor.update_selection(
936 DisplayPoint::new(DisplayRow(0), 3),
937 0,
938 gpui::Point::<f32>::default(),
939 window,
940 cx,
941 );
942 editor.end_selection(window, cx);
943 assert_eq!(
944 editor.selections.display_ranges(cx),
945 [
946 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
947 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
948 ]
949 );
950 });
951
952 _ = editor.update(cx, |editor, window, cx| {
953 editor.cancel(&Cancel, window, cx);
954 assert_eq!(
955 editor.selections.display_ranges(cx),
956 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
957 );
958 });
959
960 _ = editor.update(cx, |editor, window, cx| {
961 editor.cancel(&Cancel, window, cx);
962 assert_eq!(
963 editor.selections.display_ranges(cx),
964 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
965 );
966 });
967}
968
969#[gpui::test]
970fn test_fold_action(cx: &mut TestAppContext) {
971 init_test(cx, |_| {});
972
973 let editor = cx.add_window(|window, cx| {
974 let buffer = MultiBuffer::build_simple(
975 &"
976 impl Foo {
977 // Hello!
978
979 fn a() {
980 1
981 }
982
983 fn b() {
984 2
985 }
986
987 fn c() {
988 3
989 }
990 }
991 "
992 .unindent(),
993 cx,
994 );
995 build_editor(buffer, window, cx)
996 });
997
998 _ = editor.update(cx, |editor, window, cx| {
999 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1000 s.select_display_ranges([
1001 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1002 ]);
1003 });
1004 editor.fold(&Fold, window, cx);
1005 assert_eq!(
1006 editor.display_text(cx),
1007 "
1008 impl Foo {
1009 // Hello!
1010
1011 fn a() {
1012 1
1013 }
1014
1015 fn b() {⋯
1016 }
1017
1018 fn c() {⋯
1019 }
1020 }
1021 "
1022 .unindent(),
1023 );
1024
1025 editor.fold(&Fold, window, cx);
1026 assert_eq!(
1027 editor.display_text(cx),
1028 "
1029 impl Foo {⋯
1030 }
1031 "
1032 .unindent(),
1033 );
1034
1035 editor.unfold_lines(&UnfoldLines, window, cx);
1036 assert_eq!(
1037 editor.display_text(cx),
1038 "
1039 impl Foo {
1040 // Hello!
1041
1042 fn a() {
1043 1
1044 }
1045
1046 fn b() {⋯
1047 }
1048
1049 fn c() {⋯
1050 }
1051 }
1052 "
1053 .unindent(),
1054 );
1055
1056 editor.unfold_lines(&UnfoldLines, window, cx);
1057 assert_eq!(
1058 editor.display_text(cx),
1059 editor.buffer.read(cx).read(cx).text()
1060 );
1061 });
1062}
1063
1064#[gpui::test]
1065fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1066 init_test(cx, |_| {});
1067
1068 let editor = cx.add_window(|window, cx| {
1069 let buffer = MultiBuffer::build_simple(
1070 &"
1071 class Foo:
1072 # Hello!
1073
1074 def a():
1075 print(1)
1076
1077 def b():
1078 print(2)
1079
1080 def c():
1081 print(3)
1082 "
1083 .unindent(),
1084 cx,
1085 );
1086 build_editor(buffer, window, cx)
1087 });
1088
1089 _ = editor.update(cx, |editor, window, cx| {
1090 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1091 s.select_display_ranges([
1092 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1093 ]);
1094 });
1095 editor.fold(&Fold, window, cx);
1096 assert_eq!(
1097 editor.display_text(cx),
1098 "
1099 class Foo:
1100 # Hello!
1101
1102 def a():
1103 print(1)
1104
1105 def b():⋯
1106
1107 def c():⋯
1108 "
1109 .unindent(),
1110 );
1111
1112 editor.fold(&Fold, window, cx);
1113 assert_eq!(
1114 editor.display_text(cx),
1115 "
1116 class Foo:⋯
1117 "
1118 .unindent(),
1119 );
1120
1121 editor.unfold_lines(&UnfoldLines, window, cx);
1122 assert_eq!(
1123 editor.display_text(cx),
1124 "
1125 class Foo:
1126 # Hello!
1127
1128 def a():
1129 print(1)
1130
1131 def b():⋯
1132
1133 def c():⋯
1134 "
1135 .unindent(),
1136 );
1137
1138 editor.unfold_lines(&UnfoldLines, window, cx);
1139 assert_eq!(
1140 editor.display_text(cx),
1141 editor.buffer.read(cx).read(cx).text()
1142 );
1143 });
1144}
1145
1146#[gpui::test]
1147fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1148 init_test(cx, |_| {});
1149
1150 let editor = cx.add_window(|window, cx| {
1151 let buffer = MultiBuffer::build_simple(
1152 &"
1153 class Foo:
1154 # Hello!
1155
1156 def a():
1157 print(1)
1158
1159 def b():
1160 print(2)
1161
1162
1163 def c():
1164 print(3)
1165
1166
1167 "
1168 .unindent(),
1169 cx,
1170 );
1171 build_editor(buffer, window, cx)
1172 });
1173
1174 _ = editor.update(cx, |editor, window, cx| {
1175 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1176 s.select_display_ranges([
1177 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1178 ]);
1179 });
1180 editor.fold(&Fold, window, cx);
1181 assert_eq!(
1182 editor.display_text(cx),
1183 "
1184 class Foo:
1185 # Hello!
1186
1187 def a():
1188 print(1)
1189
1190 def b():⋯
1191
1192
1193 def c():⋯
1194
1195
1196 "
1197 .unindent(),
1198 );
1199
1200 editor.fold(&Fold, window, cx);
1201 assert_eq!(
1202 editor.display_text(cx),
1203 "
1204 class Foo:⋯
1205
1206
1207 "
1208 .unindent(),
1209 );
1210
1211 editor.unfold_lines(&UnfoldLines, window, cx);
1212 assert_eq!(
1213 editor.display_text(cx),
1214 "
1215 class Foo:
1216 # Hello!
1217
1218 def a():
1219 print(1)
1220
1221 def b():⋯
1222
1223
1224 def c():⋯
1225
1226
1227 "
1228 .unindent(),
1229 );
1230
1231 editor.unfold_lines(&UnfoldLines, window, cx);
1232 assert_eq!(
1233 editor.display_text(cx),
1234 editor.buffer.read(cx).read(cx).text()
1235 );
1236 });
1237}
1238
1239#[gpui::test]
1240fn test_fold_at_level(cx: &mut TestAppContext) {
1241 init_test(cx, |_| {});
1242
1243 let editor = cx.add_window(|window, cx| {
1244 let buffer = MultiBuffer::build_simple(
1245 &"
1246 class Foo:
1247 # Hello!
1248
1249 def a():
1250 print(1)
1251
1252 def b():
1253 print(2)
1254
1255
1256 class Bar:
1257 # World!
1258
1259 def a():
1260 print(1)
1261
1262 def b():
1263 print(2)
1264
1265
1266 "
1267 .unindent(),
1268 cx,
1269 );
1270 build_editor(buffer, window, cx)
1271 });
1272
1273 _ = editor.update(cx, |editor, window, cx| {
1274 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1275 assert_eq!(
1276 editor.display_text(cx),
1277 "
1278 class Foo:
1279 # Hello!
1280
1281 def a():⋯
1282
1283 def b():⋯
1284
1285
1286 class Bar:
1287 # World!
1288
1289 def a():⋯
1290
1291 def b():⋯
1292
1293
1294 "
1295 .unindent(),
1296 );
1297
1298 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1299 assert_eq!(
1300 editor.display_text(cx),
1301 "
1302 class Foo:⋯
1303
1304
1305 class Bar:⋯
1306
1307
1308 "
1309 .unindent(),
1310 );
1311
1312 editor.unfold_all(&UnfoldAll, window, cx);
1313 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1314 assert_eq!(
1315 editor.display_text(cx),
1316 "
1317 class Foo:
1318 # Hello!
1319
1320 def a():
1321 print(1)
1322
1323 def b():
1324 print(2)
1325
1326
1327 class Bar:
1328 # World!
1329
1330 def a():
1331 print(1)
1332
1333 def b():
1334 print(2)
1335
1336
1337 "
1338 .unindent(),
1339 );
1340
1341 assert_eq!(
1342 editor.display_text(cx),
1343 editor.buffer.read(cx).read(cx).text()
1344 );
1345 let (_, positions) = marked_text_ranges(
1346 &"
1347 class Foo:
1348 # Hello!
1349
1350 def a():
1351 print(1)
1352
1353 def b():
1354 p«riˇ»nt(2)
1355
1356
1357 class Bar:
1358 # World!
1359
1360 def a():
1361 «ˇprint(1)
1362
1363 def b():
1364 print(2)»
1365
1366
1367 "
1368 .unindent(),
1369 true,
1370 );
1371
1372 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1373 s.select_ranges(positions)
1374 });
1375
1376 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1377 assert_eq!(
1378 editor.display_text(cx),
1379 "
1380 class Foo:
1381 # Hello!
1382
1383 def a():⋯
1384
1385 def b():
1386 print(2)
1387
1388
1389 class Bar:
1390 # World!
1391
1392 def a():
1393 print(1)
1394
1395 def b():
1396 print(2)
1397
1398
1399 "
1400 .unindent(),
1401 );
1402 });
1403}
1404
1405#[gpui::test]
1406fn test_move_cursor(cx: &mut TestAppContext) {
1407 init_test(cx, |_| {});
1408
1409 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1410 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1411
1412 buffer.update(cx, |buffer, cx| {
1413 buffer.edit(
1414 vec![
1415 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1416 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1417 ],
1418 None,
1419 cx,
1420 );
1421 });
1422 _ = editor.update(cx, |editor, window, cx| {
1423 assert_eq!(
1424 editor.selections.display_ranges(cx),
1425 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1426 );
1427
1428 editor.move_down(&MoveDown, window, cx);
1429 assert_eq!(
1430 editor.selections.display_ranges(cx),
1431 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1432 );
1433
1434 editor.move_right(&MoveRight, window, cx);
1435 assert_eq!(
1436 editor.selections.display_ranges(cx),
1437 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1438 );
1439
1440 editor.move_left(&MoveLeft, window, cx);
1441 assert_eq!(
1442 editor.selections.display_ranges(cx),
1443 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1444 );
1445
1446 editor.move_up(&MoveUp, window, cx);
1447 assert_eq!(
1448 editor.selections.display_ranges(cx),
1449 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1450 );
1451
1452 editor.move_to_end(&MoveToEnd, window, cx);
1453 assert_eq!(
1454 editor.selections.display_ranges(cx),
1455 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1456 );
1457
1458 editor.move_to_beginning(&MoveToBeginning, window, cx);
1459 assert_eq!(
1460 editor.selections.display_ranges(cx),
1461 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1462 );
1463
1464 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1465 s.select_display_ranges([
1466 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1467 ]);
1468 });
1469 editor.select_to_beginning(&SelectToBeginning, window, cx);
1470 assert_eq!(
1471 editor.selections.display_ranges(cx),
1472 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1473 );
1474
1475 editor.select_to_end(&SelectToEnd, window, cx);
1476 assert_eq!(
1477 editor.selections.display_ranges(cx),
1478 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1479 );
1480 });
1481}
1482
1483#[gpui::test]
1484fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1485 init_test(cx, |_| {});
1486
1487 let editor = cx.add_window(|window, cx| {
1488 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1489 build_editor(buffer, window, cx)
1490 });
1491
1492 assert_eq!('🟥'.len_utf8(), 4);
1493 assert_eq!('α'.len_utf8(), 2);
1494
1495 _ = editor.update(cx, |editor, window, cx| {
1496 editor.fold_creases(
1497 vec![
1498 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1499 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1500 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1501 ],
1502 true,
1503 window,
1504 cx,
1505 );
1506 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1507
1508 editor.move_right(&MoveRight, window, cx);
1509 assert_eq!(
1510 editor.selections.display_ranges(cx),
1511 &[empty_range(0, "🟥".len())]
1512 );
1513 editor.move_right(&MoveRight, window, cx);
1514 assert_eq!(
1515 editor.selections.display_ranges(cx),
1516 &[empty_range(0, "🟥🟧".len())]
1517 );
1518 editor.move_right(&MoveRight, window, cx);
1519 assert_eq!(
1520 editor.selections.display_ranges(cx),
1521 &[empty_range(0, "🟥🟧⋯".len())]
1522 );
1523
1524 editor.move_down(&MoveDown, window, cx);
1525 assert_eq!(
1526 editor.selections.display_ranges(cx),
1527 &[empty_range(1, "ab⋯e".len())]
1528 );
1529 editor.move_left(&MoveLeft, window, cx);
1530 assert_eq!(
1531 editor.selections.display_ranges(cx),
1532 &[empty_range(1, "ab⋯".len())]
1533 );
1534 editor.move_left(&MoveLeft, window, cx);
1535 assert_eq!(
1536 editor.selections.display_ranges(cx),
1537 &[empty_range(1, "ab".len())]
1538 );
1539 editor.move_left(&MoveLeft, window, cx);
1540 assert_eq!(
1541 editor.selections.display_ranges(cx),
1542 &[empty_range(1, "a".len())]
1543 );
1544
1545 editor.move_down(&MoveDown, window, cx);
1546 assert_eq!(
1547 editor.selections.display_ranges(cx),
1548 &[empty_range(2, "α".len())]
1549 );
1550 editor.move_right(&MoveRight, window, cx);
1551 assert_eq!(
1552 editor.selections.display_ranges(cx),
1553 &[empty_range(2, "αβ".len())]
1554 );
1555 editor.move_right(&MoveRight, window, cx);
1556 assert_eq!(
1557 editor.selections.display_ranges(cx),
1558 &[empty_range(2, "αβ⋯".len())]
1559 );
1560 editor.move_right(&MoveRight, window, cx);
1561 assert_eq!(
1562 editor.selections.display_ranges(cx),
1563 &[empty_range(2, "αβ⋯ε".len())]
1564 );
1565
1566 editor.move_up(&MoveUp, window, cx);
1567 assert_eq!(
1568 editor.selections.display_ranges(cx),
1569 &[empty_range(1, "ab⋯e".len())]
1570 );
1571 editor.move_down(&MoveDown, window, cx);
1572 assert_eq!(
1573 editor.selections.display_ranges(cx),
1574 &[empty_range(2, "αβ⋯ε".len())]
1575 );
1576 editor.move_up(&MoveUp, window, cx);
1577 assert_eq!(
1578 editor.selections.display_ranges(cx),
1579 &[empty_range(1, "ab⋯e".len())]
1580 );
1581
1582 editor.move_up(&MoveUp, window, cx);
1583 assert_eq!(
1584 editor.selections.display_ranges(cx),
1585 &[empty_range(0, "🟥🟧".len())]
1586 );
1587 editor.move_left(&MoveLeft, window, cx);
1588 assert_eq!(
1589 editor.selections.display_ranges(cx),
1590 &[empty_range(0, "🟥".len())]
1591 );
1592 editor.move_left(&MoveLeft, window, cx);
1593 assert_eq!(
1594 editor.selections.display_ranges(cx),
1595 &[empty_range(0, "".len())]
1596 );
1597 });
1598}
1599
1600#[gpui::test]
1601fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1602 init_test(cx, |_| {});
1603
1604 let editor = cx.add_window(|window, cx| {
1605 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1606 build_editor(buffer, window, cx)
1607 });
1608 _ = editor.update(cx, |editor, window, cx| {
1609 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1610 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1611 });
1612
1613 // moving above start of document should move selection to start of document,
1614 // but the next move down should still be at the original goal_x
1615 editor.move_up(&MoveUp, window, cx);
1616 assert_eq!(
1617 editor.selections.display_ranges(cx),
1618 &[empty_range(0, "".len())]
1619 );
1620
1621 editor.move_down(&MoveDown, window, cx);
1622 assert_eq!(
1623 editor.selections.display_ranges(cx),
1624 &[empty_range(1, "abcd".len())]
1625 );
1626
1627 editor.move_down(&MoveDown, window, cx);
1628 assert_eq!(
1629 editor.selections.display_ranges(cx),
1630 &[empty_range(2, "αβγ".len())]
1631 );
1632
1633 editor.move_down(&MoveDown, window, cx);
1634 assert_eq!(
1635 editor.selections.display_ranges(cx),
1636 &[empty_range(3, "abcd".len())]
1637 );
1638
1639 editor.move_down(&MoveDown, window, cx);
1640 assert_eq!(
1641 editor.selections.display_ranges(cx),
1642 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1643 );
1644
1645 // moving past end of document should not change goal_x
1646 editor.move_down(&MoveDown, window, cx);
1647 assert_eq!(
1648 editor.selections.display_ranges(cx),
1649 &[empty_range(5, "".len())]
1650 );
1651
1652 editor.move_down(&MoveDown, window, cx);
1653 assert_eq!(
1654 editor.selections.display_ranges(cx),
1655 &[empty_range(5, "".len())]
1656 );
1657
1658 editor.move_up(&MoveUp, window, cx);
1659 assert_eq!(
1660 editor.selections.display_ranges(cx),
1661 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1662 );
1663
1664 editor.move_up(&MoveUp, window, cx);
1665 assert_eq!(
1666 editor.selections.display_ranges(cx),
1667 &[empty_range(3, "abcd".len())]
1668 );
1669
1670 editor.move_up(&MoveUp, window, cx);
1671 assert_eq!(
1672 editor.selections.display_ranges(cx),
1673 &[empty_range(2, "αβγ".len())]
1674 );
1675 });
1676}
1677
1678#[gpui::test]
1679fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1680 init_test(cx, |_| {});
1681 let move_to_beg = MoveToBeginningOfLine {
1682 stop_at_soft_wraps: true,
1683 stop_at_indent: true,
1684 };
1685
1686 let delete_to_beg = DeleteToBeginningOfLine {
1687 stop_at_indent: false,
1688 };
1689
1690 let move_to_end = MoveToEndOfLine {
1691 stop_at_soft_wraps: true,
1692 };
1693
1694 let editor = cx.add_window(|window, cx| {
1695 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1696 build_editor(buffer, window, cx)
1697 });
1698 _ = editor.update(cx, |editor, window, cx| {
1699 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1700 s.select_display_ranges([
1701 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1702 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1703 ]);
1704 });
1705 });
1706
1707 _ = editor.update(cx, |editor, window, cx| {
1708 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1709 assert_eq!(
1710 editor.selections.display_ranges(cx),
1711 &[
1712 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1713 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1714 ]
1715 );
1716 });
1717
1718 _ = editor.update(cx, |editor, window, cx| {
1719 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1720 assert_eq!(
1721 editor.selections.display_ranges(cx),
1722 &[
1723 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1724 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1725 ]
1726 );
1727 });
1728
1729 _ = editor.update(cx, |editor, window, cx| {
1730 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1731 assert_eq!(
1732 editor.selections.display_ranges(cx),
1733 &[
1734 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1735 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1736 ]
1737 );
1738 });
1739
1740 _ = editor.update(cx, |editor, window, cx| {
1741 editor.move_to_end_of_line(&move_to_end, window, cx);
1742 assert_eq!(
1743 editor.selections.display_ranges(cx),
1744 &[
1745 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1746 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1747 ]
1748 );
1749 });
1750
1751 // Moving to the end of line again is a no-op.
1752 _ = editor.update(cx, |editor, window, cx| {
1753 editor.move_to_end_of_line(&move_to_end, window, cx);
1754 assert_eq!(
1755 editor.selections.display_ranges(cx),
1756 &[
1757 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1758 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1759 ]
1760 );
1761 });
1762
1763 _ = editor.update(cx, |editor, window, cx| {
1764 editor.move_left(&MoveLeft, window, cx);
1765 editor.select_to_beginning_of_line(
1766 &SelectToBeginningOfLine {
1767 stop_at_soft_wraps: true,
1768 stop_at_indent: true,
1769 },
1770 window,
1771 cx,
1772 );
1773 assert_eq!(
1774 editor.selections.display_ranges(cx),
1775 &[
1776 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1777 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1778 ]
1779 );
1780 });
1781
1782 _ = editor.update(cx, |editor, window, cx| {
1783 editor.select_to_beginning_of_line(
1784 &SelectToBeginningOfLine {
1785 stop_at_soft_wraps: true,
1786 stop_at_indent: true,
1787 },
1788 window,
1789 cx,
1790 );
1791 assert_eq!(
1792 editor.selections.display_ranges(cx),
1793 &[
1794 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1795 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1796 ]
1797 );
1798 });
1799
1800 _ = editor.update(cx, |editor, window, cx| {
1801 editor.select_to_beginning_of_line(
1802 &SelectToBeginningOfLine {
1803 stop_at_soft_wraps: true,
1804 stop_at_indent: true,
1805 },
1806 window,
1807 cx,
1808 );
1809 assert_eq!(
1810 editor.selections.display_ranges(cx),
1811 &[
1812 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1813 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1814 ]
1815 );
1816 });
1817
1818 _ = editor.update(cx, |editor, window, cx| {
1819 editor.select_to_end_of_line(
1820 &SelectToEndOfLine {
1821 stop_at_soft_wraps: true,
1822 },
1823 window,
1824 cx,
1825 );
1826 assert_eq!(
1827 editor.selections.display_ranges(cx),
1828 &[
1829 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1830 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1831 ]
1832 );
1833 });
1834
1835 _ = editor.update(cx, |editor, window, cx| {
1836 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1837 assert_eq!(editor.display_text(cx), "ab\n de");
1838 assert_eq!(
1839 editor.selections.display_ranges(cx),
1840 &[
1841 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1842 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1843 ]
1844 );
1845 });
1846
1847 _ = editor.update(cx, |editor, window, cx| {
1848 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1849 assert_eq!(editor.display_text(cx), "\n");
1850 assert_eq!(
1851 editor.selections.display_ranges(cx),
1852 &[
1853 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1854 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1855 ]
1856 );
1857 });
1858}
1859
1860#[gpui::test]
1861fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1862 init_test(cx, |_| {});
1863 let move_to_beg = MoveToBeginningOfLine {
1864 stop_at_soft_wraps: false,
1865 stop_at_indent: false,
1866 };
1867
1868 let move_to_end = MoveToEndOfLine {
1869 stop_at_soft_wraps: false,
1870 };
1871
1872 let editor = cx.add_window(|window, cx| {
1873 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1874 build_editor(buffer, window, cx)
1875 });
1876
1877 _ = editor.update(cx, |editor, window, cx| {
1878 editor.set_wrap_width(Some(140.0.into()), cx);
1879
1880 // We expect the following lines after wrapping
1881 // ```
1882 // thequickbrownfox
1883 // jumpedoverthelazydo
1884 // gs
1885 // ```
1886 // The final `gs` was soft-wrapped onto a new line.
1887 assert_eq!(
1888 "thequickbrownfox\njumpedoverthelaz\nydogs",
1889 editor.display_text(cx),
1890 );
1891
1892 // First, let's assert behavior on the first line, that was not soft-wrapped.
1893 // Start the cursor at the `k` on the first line
1894 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1895 s.select_display_ranges([
1896 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1897 ]);
1898 });
1899
1900 // Moving to the beginning of the line should put us at the beginning of the line.
1901 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1902 assert_eq!(
1903 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1904 editor.selections.display_ranges(cx)
1905 );
1906
1907 // Moving to the end of the line should put us at the end of the line.
1908 editor.move_to_end_of_line(&move_to_end, window, cx);
1909 assert_eq!(
1910 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1911 editor.selections.display_ranges(cx)
1912 );
1913
1914 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1915 // Start the cursor at the last line (`y` that was wrapped to a new line)
1916 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1917 s.select_display_ranges([
1918 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1919 ]);
1920 });
1921
1922 // Moving to the beginning of the line should put us at the start of the second line of
1923 // display text, i.e., the `j`.
1924 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1925 assert_eq!(
1926 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1927 editor.selections.display_ranges(cx)
1928 );
1929
1930 // Moving to the beginning of the line again should be a no-op.
1931 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1932 assert_eq!(
1933 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1934 editor.selections.display_ranges(cx)
1935 );
1936
1937 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1938 // next display line.
1939 editor.move_to_end_of_line(&move_to_end, window, cx);
1940 assert_eq!(
1941 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1942 editor.selections.display_ranges(cx)
1943 );
1944
1945 // Moving to the end of the line again should be a no-op.
1946 editor.move_to_end_of_line(&move_to_end, window, cx);
1947 assert_eq!(
1948 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1949 editor.selections.display_ranges(cx)
1950 );
1951 });
1952}
1953
1954#[gpui::test]
1955fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1956 init_test(cx, |_| {});
1957
1958 let move_to_beg = MoveToBeginningOfLine {
1959 stop_at_soft_wraps: true,
1960 stop_at_indent: true,
1961 };
1962
1963 let select_to_beg = SelectToBeginningOfLine {
1964 stop_at_soft_wraps: true,
1965 stop_at_indent: true,
1966 };
1967
1968 let delete_to_beg = DeleteToBeginningOfLine {
1969 stop_at_indent: true,
1970 };
1971
1972 let move_to_end = MoveToEndOfLine {
1973 stop_at_soft_wraps: false,
1974 };
1975
1976 let editor = cx.add_window(|window, cx| {
1977 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1978 build_editor(buffer, window, cx)
1979 });
1980
1981 _ = editor.update(cx, |editor, window, cx| {
1982 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1983 s.select_display_ranges([
1984 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1985 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1986 ]);
1987 });
1988
1989 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1990 // and the second cursor at the first non-whitespace character in the line.
1991 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1992 assert_eq!(
1993 editor.selections.display_ranges(cx),
1994 &[
1995 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1996 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1997 ]
1998 );
1999
2000 // Moving to the beginning of the line again should be a no-op for the first cursor,
2001 // and should move the second cursor to the beginning of the line.
2002 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2003 assert_eq!(
2004 editor.selections.display_ranges(cx),
2005 &[
2006 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2007 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
2008 ]
2009 );
2010
2011 // Moving to the beginning of the line again should still be a no-op for the first cursor,
2012 // and should move the second cursor back to the first non-whitespace character in the line.
2013 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2014 assert_eq!(
2015 editor.selections.display_ranges(cx),
2016 &[
2017 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2018 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2019 ]
2020 );
2021
2022 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
2023 // and to the first non-whitespace character in the line for the second cursor.
2024 editor.move_to_end_of_line(&move_to_end, window, cx);
2025 editor.move_left(&MoveLeft, window, cx);
2026 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2027 assert_eq!(
2028 editor.selections.display_ranges(cx),
2029 &[
2030 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2031 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2032 ]
2033 );
2034
2035 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2036 // and should select to the beginning of the line for the second cursor.
2037 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2038 assert_eq!(
2039 editor.selections.display_ranges(cx),
2040 &[
2041 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2042 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2043 ]
2044 );
2045
2046 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2047 // and should delete to the first non-whitespace character in the line for the second cursor.
2048 editor.move_to_end_of_line(&move_to_end, window, cx);
2049 editor.move_left(&MoveLeft, window, cx);
2050 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2051 assert_eq!(editor.text(cx), "c\n f");
2052 });
2053}
2054
2055#[gpui::test]
2056fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2057 init_test(cx, |_| {});
2058
2059 let move_to_beg = MoveToBeginningOfLine {
2060 stop_at_soft_wraps: true,
2061 stop_at_indent: true,
2062 };
2063
2064 let editor = cx.add_window(|window, cx| {
2065 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2066 build_editor(buffer, window, cx)
2067 });
2068
2069 _ = editor.update(cx, |editor, window, cx| {
2070 // test cursor between line_start and indent_start
2071 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2072 s.select_display_ranges([
2073 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2074 ]);
2075 });
2076
2077 // cursor should move to line_start
2078 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2079 assert_eq!(
2080 editor.selections.display_ranges(cx),
2081 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2082 );
2083
2084 // cursor should move to indent_start
2085 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2086 assert_eq!(
2087 editor.selections.display_ranges(cx),
2088 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2089 );
2090
2091 // cursor should move to back to line_start
2092 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2093 assert_eq!(
2094 editor.selections.display_ranges(cx),
2095 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2096 );
2097 });
2098}
2099
2100#[gpui::test]
2101fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2102 init_test(cx, |_| {});
2103
2104 let editor = cx.add_window(|window, cx| {
2105 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2106 build_editor(buffer, window, cx)
2107 });
2108 _ = editor.update(cx, |editor, window, cx| {
2109 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2110 s.select_display_ranges([
2111 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2112 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2113 ])
2114 });
2115 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2116 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2117
2118 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2119 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2120
2121 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2122 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2123
2124 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2125 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2126
2127 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2128 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2129
2130 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2131 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2132
2133 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2134 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2135
2136 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2137 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2138
2139 editor.move_right(&MoveRight, window, cx);
2140 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2141 assert_selection_ranges(
2142 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2143 editor,
2144 cx,
2145 );
2146
2147 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2148 assert_selection_ranges(
2149 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2150 editor,
2151 cx,
2152 );
2153
2154 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2155 assert_selection_ranges(
2156 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2157 editor,
2158 cx,
2159 );
2160 });
2161}
2162
2163#[gpui::test]
2164fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2165 init_test(cx, |_| {});
2166
2167 let editor = cx.add_window(|window, cx| {
2168 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2169 build_editor(buffer, window, cx)
2170 });
2171
2172 _ = editor.update(cx, |editor, window, cx| {
2173 editor.set_wrap_width(Some(140.0.into()), cx);
2174 assert_eq!(
2175 editor.display_text(cx),
2176 "use one::{\n two::three::\n four::five\n};"
2177 );
2178
2179 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2180 s.select_display_ranges([
2181 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2182 ]);
2183 });
2184
2185 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2186 assert_eq!(
2187 editor.selections.display_ranges(cx),
2188 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2189 );
2190
2191 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2192 assert_eq!(
2193 editor.selections.display_ranges(cx),
2194 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2195 );
2196
2197 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2198 assert_eq!(
2199 editor.selections.display_ranges(cx),
2200 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2201 );
2202
2203 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2204 assert_eq!(
2205 editor.selections.display_ranges(cx),
2206 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2207 );
2208
2209 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2210 assert_eq!(
2211 editor.selections.display_ranges(cx),
2212 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2213 );
2214
2215 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2216 assert_eq!(
2217 editor.selections.display_ranges(cx),
2218 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2219 );
2220 });
2221}
2222
2223#[gpui::test]
2224async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2225 init_test(cx, |_| {});
2226 let mut cx = EditorTestContext::new(cx).await;
2227
2228 let line_height = cx.editor(|editor, window, _| {
2229 editor
2230 .style()
2231 .unwrap()
2232 .text
2233 .line_height_in_pixels(window.rem_size())
2234 });
2235 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2236
2237 cx.set_state(
2238 &r#"ˇone
2239 two
2240
2241 three
2242 fourˇ
2243 five
2244
2245 six"#
2246 .unindent(),
2247 );
2248
2249 cx.update_editor(|editor, window, cx| {
2250 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2251 });
2252 cx.assert_editor_state(
2253 &r#"one
2254 two
2255 ˇ
2256 three
2257 four
2258 five
2259 ˇ
2260 six"#
2261 .unindent(),
2262 );
2263
2264 cx.update_editor(|editor, window, cx| {
2265 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2266 });
2267 cx.assert_editor_state(
2268 &r#"one
2269 two
2270
2271 three
2272 four
2273 five
2274 ˇ
2275 sixˇ"#
2276 .unindent(),
2277 );
2278
2279 cx.update_editor(|editor, window, cx| {
2280 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2281 });
2282 cx.assert_editor_state(
2283 &r#"one
2284 two
2285
2286 three
2287 four
2288 five
2289
2290 sixˇ"#
2291 .unindent(),
2292 );
2293
2294 cx.update_editor(|editor, window, cx| {
2295 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2296 });
2297 cx.assert_editor_state(
2298 &r#"one
2299 two
2300
2301 three
2302 four
2303 five
2304 ˇ
2305 six"#
2306 .unindent(),
2307 );
2308
2309 cx.update_editor(|editor, window, cx| {
2310 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2311 });
2312 cx.assert_editor_state(
2313 &r#"one
2314 two
2315 ˇ
2316 three
2317 four
2318 five
2319
2320 six"#
2321 .unindent(),
2322 );
2323
2324 cx.update_editor(|editor, window, cx| {
2325 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2326 });
2327 cx.assert_editor_state(
2328 &r#"ˇone
2329 two
2330
2331 three
2332 four
2333 five
2334
2335 six"#
2336 .unindent(),
2337 );
2338}
2339
2340#[gpui::test]
2341async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2342 init_test(cx, |_| {});
2343 let mut cx = EditorTestContext::new(cx).await;
2344 let line_height = cx.editor(|editor, window, _| {
2345 editor
2346 .style()
2347 .unwrap()
2348 .text
2349 .line_height_in_pixels(window.rem_size())
2350 });
2351 let window = cx.window;
2352 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2353
2354 cx.set_state(
2355 r#"ˇone
2356 two
2357 three
2358 four
2359 five
2360 six
2361 seven
2362 eight
2363 nine
2364 ten
2365 "#,
2366 );
2367
2368 cx.update_editor(|editor, window, cx| {
2369 assert_eq!(
2370 editor.snapshot(window, cx).scroll_position(),
2371 gpui::Point::new(0., 0.)
2372 );
2373 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2374 assert_eq!(
2375 editor.snapshot(window, cx).scroll_position(),
2376 gpui::Point::new(0., 3.)
2377 );
2378 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2379 assert_eq!(
2380 editor.snapshot(window, cx).scroll_position(),
2381 gpui::Point::new(0., 6.)
2382 );
2383 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2384 assert_eq!(
2385 editor.snapshot(window, cx).scroll_position(),
2386 gpui::Point::new(0., 3.)
2387 );
2388
2389 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2390 assert_eq!(
2391 editor.snapshot(window, cx).scroll_position(),
2392 gpui::Point::new(0., 1.)
2393 );
2394 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2395 assert_eq!(
2396 editor.snapshot(window, cx).scroll_position(),
2397 gpui::Point::new(0., 3.)
2398 );
2399 });
2400}
2401
2402#[gpui::test]
2403async fn test_autoscroll(cx: &mut TestAppContext) {
2404 init_test(cx, |_| {});
2405 let mut cx = EditorTestContext::new(cx).await;
2406
2407 let line_height = cx.update_editor(|editor, window, cx| {
2408 editor.set_vertical_scroll_margin(2, cx);
2409 editor
2410 .style()
2411 .unwrap()
2412 .text
2413 .line_height_in_pixels(window.rem_size())
2414 });
2415 let window = cx.window;
2416 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2417
2418 cx.set_state(
2419 r#"ˇone
2420 two
2421 three
2422 four
2423 five
2424 six
2425 seven
2426 eight
2427 nine
2428 ten
2429 "#,
2430 );
2431 cx.update_editor(|editor, window, cx| {
2432 assert_eq!(
2433 editor.snapshot(window, cx).scroll_position(),
2434 gpui::Point::new(0., 0.0)
2435 );
2436 });
2437
2438 // Add a cursor below the visible area. Since both cursors cannot fit
2439 // on screen, the editor autoscrolls to reveal the newest cursor, and
2440 // allows the vertical scroll margin below that cursor.
2441 cx.update_editor(|editor, window, cx| {
2442 editor.change_selections(Default::default(), window, cx, |selections| {
2443 selections.select_ranges([
2444 Point::new(0, 0)..Point::new(0, 0),
2445 Point::new(6, 0)..Point::new(6, 0),
2446 ]);
2447 })
2448 });
2449 cx.update_editor(|editor, window, cx| {
2450 assert_eq!(
2451 editor.snapshot(window, cx).scroll_position(),
2452 gpui::Point::new(0., 3.0)
2453 );
2454 });
2455
2456 // Move down. The editor cursor scrolls down to track the newest cursor.
2457 cx.update_editor(|editor, window, cx| {
2458 editor.move_down(&Default::default(), window, cx);
2459 });
2460 cx.update_editor(|editor, window, cx| {
2461 assert_eq!(
2462 editor.snapshot(window, cx).scroll_position(),
2463 gpui::Point::new(0., 4.0)
2464 );
2465 });
2466
2467 // Add a cursor above the visible area. Since both cursors fit on screen,
2468 // the editor scrolls to show both.
2469 cx.update_editor(|editor, window, cx| {
2470 editor.change_selections(Default::default(), window, cx, |selections| {
2471 selections.select_ranges([
2472 Point::new(1, 0)..Point::new(1, 0),
2473 Point::new(6, 0)..Point::new(6, 0),
2474 ]);
2475 })
2476 });
2477 cx.update_editor(|editor, window, cx| {
2478 assert_eq!(
2479 editor.snapshot(window, cx).scroll_position(),
2480 gpui::Point::new(0., 1.0)
2481 );
2482 });
2483}
2484
2485#[gpui::test]
2486async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2487 init_test(cx, |_| {});
2488 let mut cx = EditorTestContext::new(cx).await;
2489
2490 let line_height = cx.editor(|editor, window, _cx| {
2491 editor
2492 .style()
2493 .unwrap()
2494 .text
2495 .line_height_in_pixels(window.rem_size())
2496 });
2497 let window = cx.window;
2498 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2499 cx.set_state(
2500 &r#"
2501 ˇone
2502 two
2503 threeˇ
2504 four
2505 five
2506 six
2507 seven
2508 eight
2509 nine
2510 ten
2511 "#
2512 .unindent(),
2513 );
2514
2515 cx.update_editor(|editor, window, cx| {
2516 editor.move_page_down(&MovePageDown::default(), window, cx)
2517 });
2518 cx.assert_editor_state(
2519 &r#"
2520 one
2521 two
2522 three
2523 ˇfour
2524 five
2525 sixˇ
2526 seven
2527 eight
2528 nine
2529 ten
2530 "#
2531 .unindent(),
2532 );
2533
2534 cx.update_editor(|editor, window, cx| {
2535 editor.move_page_down(&MovePageDown::default(), window, cx)
2536 });
2537 cx.assert_editor_state(
2538 &r#"
2539 one
2540 two
2541 three
2542 four
2543 five
2544 six
2545 ˇseven
2546 eight
2547 nineˇ
2548 ten
2549 "#
2550 .unindent(),
2551 );
2552
2553 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2554 cx.assert_editor_state(
2555 &r#"
2556 one
2557 two
2558 three
2559 ˇfour
2560 five
2561 sixˇ
2562 seven
2563 eight
2564 nine
2565 ten
2566 "#
2567 .unindent(),
2568 );
2569
2570 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2571 cx.assert_editor_state(
2572 &r#"
2573 ˇone
2574 two
2575 threeˇ
2576 four
2577 five
2578 six
2579 seven
2580 eight
2581 nine
2582 ten
2583 "#
2584 .unindent(),
2585 );
2586
2587 // Test select collapsing
2588 cx.update_editor(|editor, window, cx| {
2589 editor.move_page_down(&MovePageDown::default(), window, cx);
2590 editor.move_page_down(&MovePageDown::default(), window, cx);
2591 editor.move_page_down(&MovePageDown::default(), window, cx);
2592 });
2593 cx.assert_editor_state(
2594 &r#"
2595 one
2596 two
2597 three
2598 four
2599 five
2600 six
2601 seven
2602 eight
2603 nine
2604 ˇten
2605 ˇ"#
2606 .unindent(),
2607 );
2608}
2609
2610#[gpui::test]
2611async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2612 init_test(cx, |_| {});
2613 let mut cx = EditorTestContext::new(cx).await;
2614 cx.set_state("one «two threeˇ» four");
2615 cx.update_editor(|editor, window, cx| {
2616 editor.delete_to_beginning_of_line(
2617 &DeleteToBeginningOfLine {
2618 stop_at_indent: false,
2619 },
2620 window,
2621 cx,
2622 );
2623 assert_eq!(editor.text(cx), " four");
2624 });
2625}
2626
2627#[gpui::test]
2628async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2629 init_test(cx, |_| {});
2630
2631 let mut cx = EditorTestContext::new(cx).await;
2632
2633 // For an empty selection, the preceding word fragment is deleted.
2634 // For non-empty selections, only selected characters are deleted.
2635 cx.set_state("onˇe two t«hreˇ»e four");
2636 cx.update_editor(|editor, window, cx| {
2637 editor.delete_to_previous_word_start(
2638 &DeleteToPreviousWordStart {
2639 ignore_newlines: false,
2640 ignore_brackets: false,
2641 },
2642 window,
2643 cx,
2644 );
2645 });
2646 cx.assert_editor_state("ˇe two tˇe four");
2647
2648 cx.set_state("e tˇwo te «fˇ»our");
2649 cx.update_editor(|editor, window, cx| {
2650 editor.delete_to_next_word_end(
2651 &DeleteToNextWordEnd {
2652 ignore_newlines: false,
2653 ignore_brackets: false,
2654 },
2655 window,
2656 cx,
2657 );
2658 });
2659 cx.assert_editor_state("e tˇ te ˇour");
2660}
2661
2662#[gpui::test]
2663async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2664 init_test(cx, |_| {});
2665
2666 let mut cx = EditorTestContext::new(cx).await;
2667
2668 cx.set_state("here is some text ˇwith a space");
2669 cx.update_editor(|editor, window, cx| {
2670 editor.delete_to_previous_word_start(
2671 &DeleteToPreviousWordStart {
2672 ignore_newlines: false,
2673 ignore_brackets: true,
2674 },
2675 window,
2676 cx,
2677 );
2678 });
2679 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2680 cx.assert_editor_state("here is some textˇwith a space");
2681
2682 cx.set_state("here is some text ˇwith a space");
2683 cx.update_editor(|editor, window, cx| {
2684 editor.delete_to_previous_word_start(
2685 &DeleteToPreviousWordStart {
2686 ignore_newlines: false,
2687 ignore_brackets: false,
2688 },
2689 window,
2690 cx,
2691 );
2692 });
2693 cx.assert_editor_state("here is some textˇwith a space");
2694
2695 cx.set_state("here is some textˇ with a space");
2696 cx.update_editor(|editor, window, cx| {
2697 editor.delete_to_next_word_end(
2698 &DeleteToNextWordEnd {
2699 ignore_newlines: false,
2700 ignore_brackets: true,
2701 },
2702 window,
2703 cx,
2704 );
2705 });
2706 // Same happens in the other direction.
2707 cx.assert_editor_state("here is some textˇwith a space");
2708
2709 cx.set_state("here is some textˇ with a space");
2710 cx.update_editor(|editor, window, cx| {
2711 editor.delete_to_next_word_end(
2712 &DeleteToNextWordEnd {
2713 ignore_newlines: false,
2714 ignore_brackets: false,
2715 },
2716 window,
2717 cx,
2718 );
2719 });
2720 cx.assert_editor_state("here is some textˇwith a space");
2721
2722 cx.set_state("here is some textˇ with a space");
2723 cx.update_editor(|editor, window, cx| {
2724 editor.delete_to_next_word_end(
2725 &DeleteToNextWordEnd {
2726 ignore_newlines: true,
2727 ignore_brackets: false,
2728 },
2729 window,
2730 cx,
2731 );
2732 });
2733 cx.assert_editor_state("here is some textˇwith a space");
2734 cx.update_editor(|editor, window, cx| {
2735 editor.delete_to_previous_word_start(
2736 &DeleteToPreviousWordStart {
2737 ignore_newlines: true,
2738 ignore_brackets: false,
2739 },
2740 window,
2741 cx,
2742 );
2743 });
2744 cx.assert_editor_state("here is some ˇwith a space");
2745 cx.update_editor(|editor, window, cx| {
2746 editor.delete_to_previous_word_start(
2747 &DeleteToPreviousWordStart {
2748 ignore_newlines: true,
2749 ignore_brackets: false,
2750 },
2751 window,
2752 cx,
2753 );
2754 });
2755 // Single whitespaces are removed with the word behind them.
2756 cx.assert_editor_state("here is ˇwith a space");
2757 cx.update_editor(|editor, window, cx| {
2758 editor.delete_to_previous_word_start(
2759 &DeleteToPreviousWordStart {
2760 ignore_newlines: true,
2761 ignore_brackets: false,
2762 },
2763 window,
2764 cx,
2765 );
2766 });
2767 cx.assert_editor_state("here ˇwith a space");
2768 cx.update_editor(|editor, window, cx| {
2769 editor.delete_to_previous_word_start(
2770 &DeleteToPreviousWordStart {
2771 ignore_newlines: true,
2772 ignore_brackets: false,
2773 },
2774 window,
2775 cx,
2776 );
2777 });
2778 cx.assert_editor_state("ˇwith a space");
2779 cx.update_editor(|editor, window, cx| {
2780 editor.delete_to_previous_word_start(
2781 &DeleteToPreviousWordStart {
2782 ignore_newlines: true,
2783 ignore_brackets: false,
2784 },
2785 window,
2786 cx,
2787 );
2788 });
2789 cx.assert_editor_state("ˇwith a space");
2790 cx.update_editor(|editor, window, cx| {
2791 editor.delete_to_next_word_end(
2792 &DeleteToNextWordEnd {
2793 ignore_newlines: true,
2794 ignore_brackets: false,
2795 },
2796 window,
2797 cx,
2798 );
2799 });
2800 // Same happens in the other direction.
2801 cx.assert_editor_state("ˇ a space");
2802 cx.update_editor(|editor, window, cx| {
2803 editor.delete_to_next_word_end(
2804 &DeleteToNextWordEnd {
2805 ignore_newlines: true,
2806 ignore_brackets: false,
2807 },
2808 window,
2809 cx,
2810 );
2811 });
2812 cx.assert_editor_state("ˇ space");
2813 cx.update_editor(|editor, window, cx| {
2814 editor.delete_to_next_word_end(
2815 &DeleteToNextWordEnd {
2816 ignore_newlines: true,
2817 ignore_brackets: false,
2818 },
2819 window,
2820 cx,
2821 );
2822 });
2823 cx.assert_editor_state("ˇ");
2824 cx.update_editor(|editor, window, cx| {
2825 editor.delete_to_next_word_end(
2826 &DeleteToNextWordEnd {
2827 ignore_newlines: true,
2828 ignore_brackets: false,
2829 },
2830 window,
2831 cx,
2832 );
2833 });
2834 cx.assert_editor_state("ˇ");
2835 cx.update_editor(|editor, window, cx| {
2836 editor.delete_to_previous_word_start(
2837 &DeleteToPreviousWordStart {
2838 ignore_newlines: true,
2839 ignore_brackets: false,
2840 },
2841 window,
2842 cx,
2843 );
2844 });
2845 cx.assert_editor_state("ˇ");
2846}
2847
2848#[gpui::test]
2849async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2850 init_test(cx, |_| {});
2851
2852 let language = Arc::new(
2853 Language::new(
2854 LanguageConfig {
2855 brackets: BracketPairConfig {
2856 pairs: vec![
2857 BracketPair {
2858 start: "\"".to_string(),
2859 end: "\"".to_string(),
2860 close: true,
2861 surround: true,
2862 newline: false,
2863 },
2864 BracketPair {
2865 start: "(".to_string(),
2866 end: ")".to_string(),
2867 close: true,
2868 surround: true,
2869 newline: true,
2870 },
2871 ],
2872 ..BracketPairConfig::default()
2873 },
2874 ..LanguageConfig::default()
2875 },
2876 Some(tree_sitter_rust::LANGUAGE.into()),
2877 )
2878 .with_brackets_query(
2879 r#"
2880 ("(" @open ")" @close)
2881 ("\"" @open "\"" @close)
2882 "#,
2883 )
2884 .unwrap(),
2885 );
2886
2887 let mut cx = EditorTestContext::new(cx).await;
2888 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2889
2890 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2891 cx.update_editor(|editor, window, cx| {
2892 editor.delete_to_previous_word_start(
2893 &DeleteToPreviousWordStart {
2894 ignore_newlines: true,
2895 ignore_brackets: false,
2896 },
2897 window,
2898 cx,
2899 );
2900 });
2901 // Deletion stops before brackets if asked to not ignore them.
2902 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2903 cx.update_editor(|editor, window, cx| {
2904 editor.delete_to_previous_word_start(
2905 &DeleteToPreviousWordStart {
2906 ignore_newlines: true,
2907 ignore_brackets: false,
2908 },
2909 window,
2910 cx,
2911 );
2912 });
2913 // Deletion has to remove a single bracket and then stop again.
2914 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2915
2916 cx.update_editor(|editor, window, cx| {
2917 editor.delete_to_previous_word_start(
2918 &DeleteToPreviousWordStart {
2919 ignore_newlines: true,
2920 ignore_brackets: false,
2921 },
2922 window,
2923 cx,
2924 );
2925 });
2926 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2927
2928 cx.update_editor(|editor, window, cx| {
2929 editor.delete_to_previous_word_start(
2930 &DeleteToPreviousWordStart {
2931 ignore_newlines: true,
2932 ignore_brackets: false,
2933 },
2934 window,
2935 cx,
2936 );
2937 });
2938 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2939
2940 cx.update_editor(|editor, window, cx| {
2941 editor.delete_to_previous_word_start(
2942 &DeleteToPreviousWordStart {
2943 ignore_newlines: true,
2944 ignore_brackets: false,
2945 },
2946 window,
2947 cx,
2948 );
2949 });
2950 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2951
2952 cx.update_editor(|editor, window, cx| {
2953 editor.delete_to_next_word_end(
2954 &DeleteToNextWordEnd {
2955 ignore_newlines: true,
2956 ignore_brackets: false,
2957 },
2958 window,
2959 cx,
2960 );
2961 });
2962 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2963 cx.assert_editor_state(r#"ˇ");"#);
2964
2965 cx.update_editor(|editor, window, cx| {
2966 editor.delete_to_next_word_end(
2967 &DeleteToNextWordEnd {
2968 ignore_newlines: true,
2969 ignore_brackets: false,
2970 },
2971 window,
2972 cx,
2973 );
2974 });
2975 cx.assert_editor_state(r#"ˇ"#);
2976
2977 cx.update_editor(|editor, window, cx| {
2978 editor.delete_to_next_word_end(
2979 &DeleteToNextWordEnd {
2980 ignore_newlines: true,
2981 ignore_brackets: false,
2982 },
2983 window,
2984 cx,
2985 );
2986 });
2987 cx.assert_editor_state(r#"ˇ"#);
2988
2989 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2990 cx.update_editor(|editor, window, cx| {
2991 editor.delete_to_previous_word_start(
2992 &DeleteToPreviousWordStart {
2993 ignore_newlines: true,
2994 ignore_brackets: true,
2995 },
2996 window,
2997 cx,
2998 );
2999 });
3000 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
3001}
3002
3003#[gpui::test]
3004fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
3005 init_test(cx, |_| {});
3006
3007 let editor = cx.add_window(|window, cx| {
3008 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
3009 build_editor(buffer, window, cx)
3010 });
3011 let del_to_prev_word_start = DeleteToPreviousWordStart {
3012 ignore_newlines: false,
3013 ignore_brackets: false,
3014 };
3015 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
3016 ignore_newlines: true,
3017 ignore_brackets: false,
3018 };
3019
3020 _ = editor.update(cx, |editor, window, cx| {
3021 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3022 s.select_display_ranges([
3023 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
3024 ])
3025 });
3026 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3027 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
3028 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3029 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
3030 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3031 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
3032 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3033 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
3034 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3035 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3036 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3037 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3038 });
3039}
3040
3041#[gpui::test]
3042fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3043 init_test(cx, |_| {});
3044
3045 let editor = cx.add_window(|window, cx| {
3046 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3047 build_editor(buffer, window, cx)
3048 });
3049 let del_to_next_word_end = DeleteToNextWordEnd {
3050 ignore_newlines: false,
3051 ignore_brackets: false,
3052 };
3053 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3054 ignore_newlines: true,
3055 ignore_brackets: false,
3056 };
3057
3058 _ = editor.update(cx, |editor, window, cx| {
3059 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3060 s.select_display_ranges([
3061 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3062 ])
3063 });
3064 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3065 assert_eq!(
3066 editor.buffer.read(cx).read(cx).text(),
3067 "one\n two\nthree\n four"
3068 );
3069 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3070 assert_eq!(
3071 editor.buffer.read(cx).read(cx).text(),
3072 "\n two\nthree\n four"
3073 );
3074 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3075 assert_eq!(
3076 editor.buffer.read(cx).read(cx).text(),
3077 "two\nthree\n four"
3078 );
3079 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3080 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3081 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3082 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3083 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3084 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3085 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3086 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3087 });
3088}
3089
3090#[gpui::test]
3091fn test_newline(cx: &mut TestAppContext) {
3092 init_test(cx, |_| {});
3093
3094 let editor = cx.add_window(|window, cx| {
3095 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3096 build_editor(buffer, window, cx)
3097 });
3098
3099 _ = editor.update(cx, |editor, window, cx| {
3100 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3101 s.select_display_ranges([
3102 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3103 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3104 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3105 ])
3106 });
3107
3108 editor.newline(&Newline, window, cx);
3109 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3110 });
3111}
3112
3113#[gpui::test]
3114fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3115 init_test(cx, |_| {});
3116
3117 let editor = cx.add_window(|window, cx| {
3118 let buffer = MultiBuffer::build_simple(
3119 "
3120 a
3121 b(
3122 X
3123 )
3124 c(
3125 X
3126 )
3127 "
3128 .unindent()
3129 .as_str(),
3130 cx,
3131 );
3132 let mut editor = build_editor(buffer, window, cx);
3133 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3134 s.select_ranges([
3135 Point::new(2, 4)..Point::new(2, 5),
3136 Point::new(5, 4)..Point::new(5, 5),
3137 ])
3138 });
3139 editor
3140 });
3141
3142 _ = editor.update(cx, |editor, window, cx| {
3143 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3144 editor.buffer.update(cx, |buffer, cx| {
3145 buffer.edit(
3146 [
3147 (Point::new(1, 2)..Point::new(3, 0), ""),
3148 (Point::new(4, 2)..Point::new(6, 0), ""),
3149 ],
3150 None,
3151 cx,
3152 );
3153 assert_eq!(
3154 buffer.read(cx).text(),
3155 "
3156 a
3157 b()
3158 c()
3159 "
3160 .unindent()
3161 );
3162 });
3163 assert_eq!(
3164 editor.selections.ranges(cx),
3165 &[
3166 Point::new(1, 2)..Point::new(1, 2),
3167 Point::new(2, 2)..Point::new(2, 2),
3168 ],
3169 );
3170
3171 editor.newline(&Newline, window, cx);
3172 assert_eq!(
3173 editor.text(cx),
3174 "
3175 a
3176 b(
3177 )
3178 c(
3179 )
3180 "
3181 .unindent()
3182 );
3183
3184 // The selections are moved after the inserted newlines
3185 assert_eq!(
3186 editor.selections.ranges(cx),
3187 &[
3188 Point::new(2, 0)..Point::new(2, 0),
3189 Point::new(4, 0)..Point::new(4, 0),
3190 ],
3191 );
3192 });
3193}
3194
3195#[gpui::test]
3196async fn test_newline_above(cx: &mut TestAppContext) {
3197 init_test(cx, |settings| {
3198 settings.defaults.tab_size = NonZeroU32::new(4)
3199 });
3200
3201 let language = Arc::new(
3202 Language::new(
3203 LanguageConfig::default(),
3204 Some(tree_sitter_rust::LANGUAGE.into()),
3205 )
3206 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3207 .unwrap(),
3208 );
3209
3210 let mut cx = EditorTestContext::new(cx).await;
3211 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3212 cx.set_state(indoc! {"
3213 const a: ˇA = (
3214 (ˇ
3215 «const_functionˇ»(ˇ),
3216 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3217 )ˇ
3218 ˇ);ˇ
3219 "});
3220
3221 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3222 cx.assert_editor_state(indoc! {"
3223 ˇ
3224 const a: A = (
3225 ˇ
3226 (
3227 ˇ
3228 ˇ
3229 const_function(),
3230 ˇ
3231 ˇ
3232 ˇ
3233 ˇ
3234 something_else,
3235 ˇ
3236 )
3237 ˇ
3238 ˇ
3239 );
3240 "});
3241}
3242
3243#[gpui::test]
3244async fn test_newline_below(cx: &mut TestAppContext) {
3245 init_test(cx, |settings| {
3246 settings.defaults.tab_size = NonZeroU32::new(4)
3247 });
3248
3249 let language = Arc::new(
3250 Language::new(
3251 LanguageConfig::default(),
3252 Some(tree_sitter_rust::LANGUAGE.into()),
3253 )
3254 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3255 .unwrap(),
3256 );
3257
3258 let mut cx = EditorTestContext::new(cx).await;
3259 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3260 cx.set_state(indoc! {"
3261 const a: ˇA = (
3262 (ˇ
3263 «const_functionˇ»(ˇ),
3264 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3265 )ˇ
3266 ˇ);ˇ
3267 "});
3268
3269 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3270 cx.assert_editor_state(indoc! {"
3271 const a: A = (
3272 ˇ
3273 (
3274 ˇ
3275 const_function(),
3276 ˇ
3277 ˇ
3278 something_else,
3279 ˇ
3280 ˇ
3281 ˇ
3282 ˇ
3283 )
3284 ˇ
3285 );
3286 ˇ
3287 ˇ
3288 "});
3289}
3290
3291#[gpui::test]
3292async fn test_newline_comments(cx: &mut TestAppContext) {
3293 init_test(cx, |settings| {
3294 settings.defaults.tab_size = NonZeroU32::new(4)
3295 });
3296
3297 let language = Arc::new(Language::new(
3298 LanguageConfig {
3299 line_comments: vec!["// ".into()],
3300 ..LanguageConfig::default()
3301 },
3302 None,
3303 ));
3304 {
3305 let mut cx = EditorTestContext::new(cx).await;
3306 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3307 cx.set_state(indoc! {"
3308 // Fooˇ
3309 "});
3310
3311 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3312 cx.assert_editor_state(indoc! {"
3313 // Foo
3314 // ˇ
3315 "});
3316 // Ensure that we add comment prefix when existing line contains space
3317 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3318 cx.assert_editor_state(
3319 indoc! {"
3320 // Foo
3321 //s
3322 // ˇ
3323 "}
3324 .replace("s", " ") // s is used as space placeholder to prevent format on save
3325 .as_str(),
3326 );
3327 // Ensure that we add comment prefix when existing line does not contain space
3328 cx.set_state(indoc! {"
3329 // Foo
3330 //ˇ
3331 "});
3332 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3333 cx.assert_editor_state(indoc! {"
3334 // Foo
3335 //
3336 // ˇ
3337 "});
3338 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3339 cx.set_state(indoc! {"
3340 ˇ// Foo
3341 "});
3342 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3343 cx.assert_editor_state(indoc! {"
3344
3345 ˇ// Foo
3346 "});
3347 }
3348 // Ensure that comment continuations can be disabled.
3349 update_test_language_settings(cx, |settings| {
3350 settings.defaults.extend_comment_on_newline = Some(false);
3351 });
3352 let mut cx = EditorTestContext::new(cx).await;
3353 cx.set_state(indoc! {"
3354 // Fooˇ
3355 "});
3356 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3357 cx.assert_editor_state(indoc! {"
3358 // Foo
3359 ˇ
3360 "});
3361}
3362
3363#[gpui::test]
3364async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3365 init_test(cx, |settings| {
3366 settings.defaults.tab_size = NonZeroU32::new(4)
3367 });
3368
3369 let language = Arc::new(Language::new(
3370 LanguageConfig {
3371 line_comments: vec!["// ".into(), "/// ".into()],
3372 ..LanguageConfig::default()
3373 },
3374 None,
3375 ));
3376 {
3377 let mut cx = EditorTestContext::new(cx).await;
3378 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3379 cx.set_state(indoc! {"
3380 //ˇ
3381 "});
3382 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3383 cx.assert_editor_state(indoc! {"
3384 //
3385 // ˇ
3386 "});
3387
3388 cx.set_state(indoc! {"
3389 ///ˇ
3390 "});
3391 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3392 cx.assert_editor_state(indoc! {"
3393 ///
3394 /// ˇ
3395 "});
3396 }
3397}
3398
3399#[gpui::test]
3400async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3401 init_test(cx, |settings| {
3402 settings.defaults.tab_size = NonZeroU32::new(4)
3403 });
3404
3405 let language = Arc::new(
3406 Language::new(
3407 LanguageConfig {
3408 documentation_comment: Some(language::BlockCommentConfig {
3409 start: "/**".into(),
3410 end: "*/".into(),
3411 prefix: "* ".into(),
3412 tab_size: 1,
3413 }),
3414
3415 ..LanguageConfig::default()
3416 },
3417 Some(tree_sitter_rust::LANGUAGE.into()),
3418 )
3419 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3420 .unwrap(),
3421 );
3422
3423 {
3424 let mut cx = EditorTestContext::new(cx).await;
3425 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3426 cx.set_state(indoc! {"
3427 /**ˇ
3428 "});
3429
3430 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3431 cx.assert_editor_state(indoc! {"
3432 /**
3433 * ˇ
3434 "});
3435 // Ensure that if cursor is before the comment start,
3436 // we do not actually insert a comment prefix.
3437 cx.set_state(indoc! {"
3438 ˇ/**
3439 "});
3440 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3441 cx.assert_editor_state(indoc! {"
3442
3443 ˇ/**
3444 "});
3445 // Ensure that if cursor is between it doesn't add comment prefix.
3446 cx.set_state(indoc! {"
3447 /*ˇ*
3448 "});
3449 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3450 cx.assert_editor_state(indoc! {"
3451 /*
3452 ˇ*
3453 "});
3454 // Ensure that if suffix exists on same line after cursor it adds new line.
3455 cx.set_state(indoc! {"
3456 /**ˇ*/
3457 "});
3458 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3459 cx.assert_editor_state(indoc! {"
3460 /**
3461 * ˇ
3462 */
3463 "});
3464 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3465 cx.set_state(indoc! {"
3466 /**ˇ */
3467 "});
3468 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3469 cx.assert_editor_state(indoc! {"
3470 /**
3471 * ˇ
3472 */
3473 "});
3474 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3475 cx.set_state(indoc! {"
3476 /** ˇ*/
3477 "});
3478 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3479 cx.assert_editor_state(
3480 indoc! {"
3481 /**s
3482 * ˇ
3483 */
3484 "}
3485 .replace("s", " ") // s is used as space placeholder to prevent format on save
3486 .as_str(),
3487 );
3488 // Ensure that delimiter space is preserved when newline on already
3489 // spaced delimiter.
3490 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3491 cx.assert_editor_state(
3492 indoc! {"
3493 /**s
3494 *s
3495 * ˇ
3496 */
3497 "}
3498 .replace("s", " ") // s is used as space placeholder to prevent format on save
3499 .as_str(),
3500 );
3501 // Ensure that delimiter space is preserved when space is not
3502 // on existing delimiter.
3503 cx.set_state(indoc! {"
3504 /**
3505 *ˇ
3506 */
3507 "});
3508 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3509 cx.assert_editor_state(indoc! {"
3510 /**
3511 *
3512 * ˇ
3513 */
3514 "});
3515 // Ensure that if suffix exists on same line after cursor it
3516 // doesn't add extra new line if prefix is not on same line.
3517 cx.set_state(indoc! {"
3518 /**
3519 ˇ*/
3520 "});
3521 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3522 cx.assert_editor_state(indoc! {"
3523 /**
3524
3525 ˇ*/
3526 "});
3527 // Ensure that it detects suffix after existing prefix.
3528 cx.set_state(indoc! {"
3529 /**ˇ/
3530 "});
3531 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3532 cx.assert_editor_state(indoc! {"
3533 /**
3534 ˇ/
3535 "});
3536 // Ensure that if suffix exists on same line before
3537 // cursor it does not add comment prefix.
3538 cx.set_state(indoc! {"
3539 /** */ˇ
3540 "});
3541 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3542 cx.assert_editor_state(indoc! {"
3543 /** */
3544 ˇ
3545 "});
3546 // Ensure that if suffix exists on same line before
3547 // cursor it does not add comment prefix.
3548 cx.set_state(indoc! {"
3549 /**
3550 *
3551 */ˇ
3552 "});
3553 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3554 cx.assert_editor_state(indoc! {"
3555 /**
3556 *
3557 */
3558 ˇ
3559 "});
3560
3561 // Ensure that inline comment followed by code
3562 // doesn't add comment prefix on newline
3563 cx.set_state(indoc! {"
3564 /** */ textˇ
3565 "});
3566 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3567 cx.assert_editor_state(indoc! {"
3568 /** */ text
3569 ˇ
3570 "});
3571
3572 // Ensure that text after comment end tag
3573 // doesn't add comment prefix on newline
3574 cx.set_state(indoc! {"
3575 /**
3576 *
3577 */ˇtext
3578 "});
3579 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3580 cx.assert_editor_state(indoc! {"
3581 /**
3582 *
3583 */
3584 ˇtext
3585 "});
3586
3587 // Ensure if not comment block it doesn't
3588 // add comment prefix on newline
3589 cx.set_state(indoc! {"
3590 * textˇ
3591 "});
3592 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3593 cx.assert_editor_state(indoc! {"
3594 * text
3595 ˇ
3596 "});
3597 }
3598 // Ensure that comment continuations can be disabled.
3599 update_test_language_settings(cx, |settings| {
3600 settings.defaults.extend_comment_on_newline = Some(false);
3601 });
3602 let mut cx = EditorTestContext::new(cx).await;
3603 cx.set_state(indoc! {"
3604 /**ˇ
3605 "});
3606 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3607 cx.assert_editor_state(indoc! {"
3608 /**
3609 ˇ
3610 "});
3611}
3612
3613#[gpui::test]
3614async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3615 init_test(cx, |settings| {
3616 settings.defaults.tab_size = NonZeroU32::new(4)
3617 });
3618
3619 let lua_language = Arc::new(Language::new(
3620 LanguageConfig {
3621 line_comments: vec!["--".into()],
3622 block_comment: Some(language::BlockCommentConfig {
3623 start: "--[[".into(),
3624 prefix: "".into(),
3625 end: "]]".into(),
3626 tab_size: 0,
3627 }),
3628 ..LanguageConfig::default()
3629 },
3630 None,
3631 ));
3632
3633 let mut cx = EditorTestContext::new(cx).await;
3634 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3635
3636 // Line with line comment should extend
3637 cx.set_state(indoc! {"
3638 --ˇ
3639 "});
3640 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3641 cx.assert_editor_state(indoc! {"
3642 --
3643 --ˇ
3644 "});
3645
3646 // Line with block comment that matches line comment should not extend
3647 cx.set_state(indoc! {"
3648 --[[ˇ
3649 "});
3650 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3651 cx.assert_editor_state(indoc! {"
3652 --[[
3653 ˇ
3654 "});
3655}
3656
3657#[gpui::test]
3658fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3659 init_test(cx, |_| {});
3660
3661 let editor = cx.add_window(|window, cx| {
3662 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3663 let mut editor = build_editor(buffer, window, cx);
3664 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3665 s.select_ranges([3..4, 11..12, 19..20])
3666 });
3667 editor
3668 });
3669
3670 _ = editor.update(cx, |editor, window, cx| {
3671 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3672 editor.buffer.update(cx, |buffer, cx| {
3673 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3674 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3675 });
3676 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3677
3678 editor.insert("Z", window, cx);
3679 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3680
3681 // The selections are moved after the inserted characters
3682 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3683 });
3684}
3685
3686#[gpui::test]
3687async fn test_tab(cx: &mut TestAppContext) {
3688 init_test(cx, |settings| {
3689 settings.defaults.tab_size = NonZeroU32::new(3)
3690 });
3691
3692 let mut cx = EditorTestContext::new(cx).await;
3693 cx.set_state(indoc! {"
3694 ˇabˇc
3695 ˇ🏀ˇ🏀ˇefg
3696 dˇ
3697 "});
3698 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3699 cx.assert_editor_state(indoc! {"
3700 ˇab ˇc
3701 ˇ🏀 ˇ🏀 ˇefg
3702 d ˇ
3703 "});
3704
3705 cx.set_state(indoc! {"
3706 a
3707 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3708 "});
3709 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3710 cx.assert_editor_state(indoc! {"
3711 a
3712 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3713 "});
3714}
3715
3716#[gpui::test]
3717async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3718 init_test(cx, |_| {});
3719
3720 let mut cx = EditorTestContext::new(cx).await;
3721 let language = Arc::new(
3722 Language::new(
3723 LanguageConfig::default(),
3724 Some(tree_sitter_rust::LANGUAGE.into()),
3725 )
3726 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3727 .unwrap(),
3728 );
3729 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3730
3731 // test when all cursors are not at suggested indent
3732 // then simply move to their suggested indent location
3733 cx.set_state(indoc! {"
3734 const a: B = (
3735 c(
3736 ˇ
3737 ˇ )
3738 );
3739 "});
3740 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3741 cx.assert_editor_state(indoc! {"
3742 const a: B = (
3743 c(
3744 ˇ
3745 ˇ)
3746 );
3747 "});
3748
3749 // test cursor already at suggested indent not moving when
3750 // other cursors are yet to reach their suggested indents
3751 cx.set_state(indoc! {"
3752 ˇ
3753 const a: B = (
3754 c(
3755 d(
3756 ˇ
3757 )
3758 ˇ
3759 ˇ )
3760 );
3761 "});
3762 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3763 cx.assert_editor_state(indoc! {"
3764 ˇ
3765 const a: B = (
3766 c(
3767 d(
3768 ˇ
3769 )
3770 ˇ
3771 ˇ)
3772 );
3773 "});
3774 // test when all cursors are at suggested indent then tab is inserted
3775 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3776 cx.assert_editor_state(indoc! {"
3777 ˇ
3778 const a: B = (
3779 c(
3780 d(
3781 ˇ
3782 )
3783 ˇ
3784 ˇ)
3785 );
3786 "});
3787
3788 // test when current indent is less than suggested indent,
3789 // we adjust line to match suggested indent and move cursor to it
3790 //
3791 // when no other cursor is at word boundary, all of them should move
3792 cx.set_state(indoc! {"
3793 const a: B = (
3794 c(
3795 d(
3796 ˇ
3797 ˇ )
3798 ˇ )
3799 );
3800 "});
3801 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3802 cx.assert_editor_state(indoc! {"
3803 const a: B = (
3804 c(
3805 d(
3806 ˇ
3807 ˇ)
3808 ˇ)
3809 );
3810 "});
3811
3812 // test when current indent is less than suggested indent,
3813 // we adjust line to match suggested indent and move cursor to it
3814 //
3815 // when some other cursor is at word boundary, it should not move
3816 cx.set_state(indoc! {"
3817 const a: B = (
3818 c(
3819 d(
3820 ˇ
3821 ˇ )
3822 ˇ)
3823 );
3824 "});
3825 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3826 cx.assert_editor_state(indoc! {"
3827 const a: B = (
3828 c(
3829 d(
3830 ˇ
3831 ˇ)
3832 ˇ)
3833 );
3834 "});
3835
3836 // test when current indent is more than suggested indent,
3837 // we just move cursor to current indent instead of suggested indent
3838 //
3839 // when no other cursor is at word boundary, all of them should move
3840 cx.set_state(indoc! {"
3841 const a: B = (
3842 c(
3843 d(
3844 ˇ
3845 ˇ )
3846 ˇ )
3847 );
3848 "});
3849 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3850 cx.assert_editor_state(indoc! {"
3851 const a: B = (
3852 c(
3853 d(
3854 ˇ
3855 ˇ)
3856 ˇ)
3857 );
3858 "});
3859 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3860 cx.assert_editor_state(indoc! {"
3861 const a: B = (
3862 c(
3863 d(
3864 ˇ
3865 ˇ)
3866 ˇ)
3867 );
3868 "});
3869
3870 // test when current indent is more than suggested indent,
3871 // we just move cursor to current indent instead of suggested indent
3872 //
3873 // when some other cursor is at word boundary, it doesn't move
3874 cx.set_state(indoc! {"
3875 const a: B = (
3876 c(
3877 d(
3878 ˇ
3879 ˇ )
3880 ˇ)
3881 );
3882 "});
3883 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3884 cx.assert_editor_state(indoc! {"
3885 const a: B = (
3886 c(
3887 d(
3888 ˇ
3889 ˇ)
3890 ˇ)
3891 );
3892 "});
3893
3894 // handle auto-indent when there are multiple cursors on the same line
3895 cx.set_state(indoc! {"
3896 const a: B = (
3897 c(
3898 ˇ ˇ
3899 ˇ )
3900 );
3901 "});
3902 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3903 cx.assert_editor_state(indoc! {"
3904 const a: B = (
3905 c(
3906 ˇ
3907 ˇ)
3908 );
3909 "});
3910}
3911
3912#[gpui::test]
3913async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3914 init_test(cx, |settings| {
3915 settings.defaults.tab_size = NonZeroU32::new(3)
3916 });
3917
3918 let mut cx = EditorTestContext::new(cx).await;
3919 cx.set_state(indoc! {"
3920 ˇ
3921 \t ˇ
3922 \t ˇ
3923 \t ˇ
3924 \t \t\t \t \t\t \t\t \t \t ˇ
3925 "});
3926
3927 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3928 cx.assert_editor_state(indoc! {"
3929 ˇ
3930 \t ˇ
3931 \t ˇ
3932 \t ˇ
3933 \t \t\t \t \t\t \t\t \t \t ˇ
3934 "});
3935}
3936
3937#[gpui::test]
3938async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3939 init_test(cx, |settings| {
3940 settings.defaults.tab_size = NonZeroU32::new(4)
3941 });
3942
3943 let language = Arc::new(
3944 Language::new(
3945 LanguageConfig::default(),
3946 Some(tree_sitter_rust::LANGUAGE.into()),
3947 )
3948 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3949 .unwrap(),
3950 );
3951
3952 let mut cx = EditorTestContext::new(cx).await;
3953 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3954 cx.set_state(indoc! {"
3955 fn a() {
3956 if b {
3957 \t ˇc
3958 }
3959 }
3960 "});
3961
3962 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3963 cx.assert_editor_state(indoc! {"
3964 fn a() {
3965 if b {
3966 ˇc
3967 }
3968 }
3969 "});
3970}
3971
3972#[gpui::test]
3973async fn test_indent_outdent(cx: &mut TestAppContext) {
3974 init_test(cx, |settings| {
3975 settings.defaults.tab_size = NonZeroU32::new(4);
3976 });
3977
3978 let mut cx = EditorTestContext::new(cx).await;
3979
3980 cx.set_state(indoc! {"
3981 «oneˇ» «twoˇ»
3982 three
3983 four
3984 "});
3985 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3986 cx.assert_editor_state(indoc! {"
3987 «oneˇ» «twoˇ»
3988 three
3989 four
3990 "});
3991
3992 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3993 cx.assert_editor_state(indoc! {"
3994 «oneˇ» «twoˇ»
3995 three
3996 four
3997 "});
3998
3999 // select across line ending
4000 cx.set_state(indoc! {"
4001 one two
4002 t«hree
4003 ˇ» four
4004 "});
4005 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4006 cx.assert_editor_state(indoc! {"
4007 one two
4008 t«hree
4009 ˇ» four
4010 "});
4011
4012 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4013 cx.assert_editor_state(indoc! {"
4014 one two
4015 t«hree
4016 ˇ» four
4017 "});
4018
4019 // Ensure that indenting/outdenting works when the cursor is at column 0.
4020 cx.set_state(indoc! {"
4021 one two
4022 ˇthree
4023 four
4024 "});
4025 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4026 cx.assert_editor_state(indoc! {"
4027 one two
4028 ˇthree
4029 four
4030 "});
4031
4032 cx.set_state(indoc! {"
4033 one two
4034 ˇ three
4035 four
4036 "});
4037 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4038 cx.assert_editor_state(indoc! {"
4039 one two
4040 ˇthree
4041 four
4042 "});
4043}
4044
4045#[gpui::test]
4046async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4047 // This is a regression test for issue #33761
4048 init_test(cx, |_| {});
4049
4050 let mut cx = EditorTestContext::new(cx).await;
4051 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4052 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4053
4054 cx.set_state(
4055 r#"ˇ# ingress:
4056ˇ# api:
4057ˇ# enabled: false
4058ˇ# pathType: Prefix
4059ˇ# console:
4060ˇ# enabled: false
4061ˇ# pathType: Prefix
4062"#,
4063 );
4064
4065 // Press tab to indent all lines
4066 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4067
4068 cx.assert_editor_state(
4069 r#" ˇ# ingress:
4070 ˇ# api:
4071 ˇ# enabled: false
4072 ˇ# pathType: Prefix
4073 ˇ# console:
4074 ˇ# enabled: false
4075 ˇ# pathType: Prefix
4076"#,
4077 );
4078}
4079
4080#[gpui::test]
4081async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4082 // This is a test to make sure our fix for issue #33761 didn't break anything
4083 init_test(cx, |_| {});
4084
4085 let mut cx = EditorTestContext::new(cx).await;
4086 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4087 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4088
4089 cx.set_state(
4090 r#"ˇingress:
4091ˇ api:
4092ˇ enabled: false
4093ˇ pathType: Prefix
4094"#,
4095 );
4096
4097 // Press tab to indent all lines
4098 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4099
4100 cx.assert_editor_state(
4101 r#"ˇingress:
4102 ˇapi:
4103 ˇenabled: false
4104 ˇpathType: Prefix
4105"#,
4106 );
4107}
4108
4109#[gpui::test]
4110async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4111 init_test(cx, |settings| {
4112 settings.defaults.hard_tabs = Some(true);
4113 });
4114
4115 let mut cx = EditorTestContext::new(cx).await;
4116
4117 // select two ranges on one line
4118 cx.set_state(indoc! {"
4119 «oneˇ» «twoˇ»
4120 three
4121 four
4122 "});
4123 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4124 cx.assert_editor_state(indoc! {"
4125 \t«oneˇ» «twoˇ»
4126 three
4127 four
4128 "});
4129 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4130 cx.assert_editor_state(indoc! {"
4131 \t\t«oneˇ» «twoˇ»
4132 three
4133 four
4134 "});
4135 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4136 cx.assert_editor_state(indoc! {"
4137 \t«oneˇ» «twoˇ»
4138 three
4139 four
4140 "});
4141 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4142 cx.assert_editor_state(indoc! {"
4143 «oneˇ» «twoˇ»
4144 three
4145 four
4146 "});
4147
4148 // select across a line ending
4149 cx.set_state(indoc! {"
4150 one two
4151 t«hree
4152 ˇ»four
4153 "});
4154 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4155 cx.assert_editor_state(indoc! {"
4156 one two
4157 \tt«hree
4158 ˇ»four
4159 "});
4160 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4161 cx.assert_editor_state(indoc! {"
4162 one two
4163 \t\tt«hree
4164 ˇ»four
4165 "});
4166 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4167 cx.assert_editor_state(indoc! {"
4168 one two
4169 \tt«hree
4170 ˇ»four
4171 "});
4172 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4173 cx.assert_editor_state(indoc! {"
4174 one two
4175 t«hree
4176 ˇ»four
4177 "});
4178
4179 // Ensure that indenting/outdenting works when the cursor is at column 0.
4180 cx.set_state(indoc! {"
4181 one two
4182 ˇthree
4183 four
4184 "});
4185 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4186 cx.assert_editor_state(indoc! {"
4187 one two
4188 ˇthree
4189 four
4190 "});
4191 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4192 cx.assert_editor_state(indoc! {"
4193 one two
4194 \tˇthree
4195 four
4196 "});
4197 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4198 cx.assert_editor_state(indoc! {"
4199 one two
4200 ˇthree
4201 four
4202 "});
4203}
4204
4205#[gpui::test]
4206fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4207 init_test(cx, |settings| {
4208 settings.languages.0.extend([
4209 (
4210 "TOML".into(),
4211 LanguageSettingsContent {
4212 tab_size: NonZeroU32::new(2),
4213 ..Default::default()
4214 },
4215 ),
4216 (
4217 "Rust".into(),
4218 LanguageSettingsContent {
4219 tab_size: NonZeroU32::new(4),
4220 ..Default::default()
4221 },
4222 ),
4223 ]);
4224 });
4225
4226 let toml_language = Arc::new(Language::new(
4227 LanguageConfig {
4228 name: "TOML".into(),
4229 ..Default::default()
4230 },
4231 None,
4232 ));
4233 let rust_language = Arc::new(Language::new(
4234 LanguageConfig {
4235 name: "Rust".into(),
4236 ..Default::default()
4237 },
4238 None,
4239 ));
4240
4241 let toml_buffer =
4242 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4243 let rust_buffer =
4244 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4245 let multibuffer = cx.new(|cx| {
4246 let mut multibuffer = MultiBuffer::new(ReadWrite);
4247 multibuffer.push_excerpts(
4248 toml_buffer.clone(),
4249 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4250 cx,
4251 );
4252 multibuffer.push_excerpts(
4253 rust_buffer.clone(),
4254 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4255 cx,
4256 );
4257 multibuffer
4258 });
4259
4260 cx.add_window(|window, cx| {
4261 let mut editor = build_editor(multibuffer, window, cx);
4262
4263 assert_eq!(
4264 editor.text(cx),
4265 indoc! {"
4266 a = 1
4267 b = 2
4268
4269 const c: usize = 3;
4270 "}
4271 );
4272
4273 select_ranges(
4274 &mut editor,
4275 indoc! {"
4276 «aˇ» = 1
4277 b = 2
4278
4279 «const c:ˇ» usize = 3;
4280 "},
4281 window,
4282 cx,
4283 );
4284
4285 editor.tab(&Tab, window, cx);
4286 assert_text_with_selections(
4287 &mut editor,
4288 indoc! {"
4289 «aˇ» = 1
4290 b = 2
4291
4292 «const c:ˇ» usize = 3;
4293 "},
4294 cx,
4295 );
4296 editor.backtab(&Backtab, window, cx);
4297 assert_text_with_selections(
4298 &mut editor,
4299 indoc! {"
4300 «aˇ» = 1
4301 b = 2
4302
4303 «const c:ˇ» usize = 3;
4304 "},
4305 cx,
4306 );
4307
4308 editor
4309 });
4310}
4311
4312#[gpui::test]
4313async fn test_backspace(cx: &mut TestAppContext) {
4314 init_test(cx, |_| {});
4315
4316 let mut cx = EditorTestContext::new(cx).await;
4317
4318 // Basic backspace
4319 cx.set_state(indoc! {"
4320 onˇe two three
4321 fou«rˇ» five six
4322 seven «ˇeight nine
4323 »ten
4324 "});
4325 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4326 cx.assert_editor_state(indoc! {"
4327 oˇe two three
4328 fouˇ five six
4329 seven ˇten
4330 "});
4331
4332 // Test backspace inside and around indents
4333 cx.set_state(indoc! {"
4334 zero
4335 ˇone
4336 ˇtwo
4337 ˇ ˇ ˇ three
4338 ˇ ˇ four
4339 "});
4340 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4341 cx.assert_editor_state(indoc! {"
4342 zero
4343 ˇone
4344 ˇtwo
4345 ˇ threeˇ four
4346 "});
4347}
4348
4349#[gpui::test]
4350async fn test_delete(cx: &mut TestAppContext) {
4351 init_test(cx, |_| {});
4352
4353 let mut cx = EditorTestContext::new(cx).await;
4354 cx.set_state(indoc! {"
4355 onˇe two three
4356 fou«rˇ» five six
4357 seven «ˇeight nine
4358 »ten
4359 "});
4360 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4361 cx.assert_editor_state(indoc! {"
4362 onˇ two three
4363 fouˇ five six
4364 seven ˇten
4365 "});
4366}
4367
4368#[gpui::test]
4369fn test_delete_line(cx: &mut TestAppContext) {
4370 init_test(cx, |_| {});
4371
4372 let editor = cx.add_window(|window, cx| {
4373 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4374 build_editor(buffer, window, cx)
4375 });
4376 _ = editor.update(cx, |editor, window, cx| {
4377 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4378 s.select_display_ranges([
4379 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4380 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4381 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4382 ])
4383 });
4384 editor.delete_line(&DeleteLine, window, cx);
4385 assert_eq!(editor.display_text(cx), "ghi");
4386 assert_eq!(
4387 editor.selections.display_ranges(cx),
4388 vec![
4389 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4390 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4391 ]
4392 );
4393 });
4394
4395 let editor = cx.add_window(|window, cx| {
4396 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4397 build_editor(buffer, window, cx)
4398 });
4399 _ = editor.update(cx, |editor, window, cx| {
4400 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4401 s.select_display_ranges([
4402 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4403 ])
4404 });
4405 editor.delete_line(&DeleteLine, window, cx);
4406 assert_eq!(editor.display_text(cx), "ghi\n");
4407 assert_eq!(
4408 editor.selections.display_ranges(cx),
4409 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4410 );
4411 });
4412
4413 let editor = cx.add_window(|window, cx| {
4414 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4415 build_editor(buffer, window, cx)
4416 });
4417 _ = editor.update(cx, |editor, window, cx| {
4418 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4419 s.select_display_ranges([
4420 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4421 ])
4422 });
4423 editor.delete_line(&DeleteLine, window, cx);
4424 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4425 assert_eq!(
4426 editor.selections.display_ranges(cx),
4427 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4428 );
4429 });
4430}
4431
4432#[gpui::test]
4433fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4434 init_test(cx, |_| {});
4435
4436 cx.add_window(|window, cx| {
4437 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4438 let mut editor = build_editor(buffer.clone(), window, cx);
4439 let buffer = buffer.read(cx).as_singleton().unwrap();
4440
4441 assert_eq!(
4442 editor.selections.ranges::<Point>(cx),
4443 &[Point::new(0, 0)..Point::new(0, 0)]
4444 );
4445
4446 // When on single line, replace newline at end by space
4447 editor.join_lines(&JoinLines, window, cx);
4448 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4449 assert_eq!(
4450 editor.selections.ranges::<Point>(cx),
4451 &[Point::new(0, 3)..Point::new(0, 3)]
4452 );
4453
4454 // When multiple lines are selected, remove newlines that are spanned by the selection
4455 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4456 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4457 });
4458 editor.join_lines(&JoinLines, window, cx);
4459 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4460 assert_eq!(
4461 editor.selections.ranges::<Point>(cx),
4462 &[Point::new(0, 11)..Point::new(0, 11)]
4463 );
4464
4465 // Undo should be transactional
4466 editor.undo(&Undo, window, cx);
4467 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4468 assert_eq!(
4469 editor.selections.ranges::<Point>(cx),
4470 &[Point::new(0, 5)..Point::new(2, 2)]
4471 );
4472
4473 // When joining an empty line don't insert a space
4474 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4475 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4476 });
4477 editor.join_lines(&JoinLines, window, cx);
4478 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4479 assert_eq!(
4480 editor.selections.ranges::<Point>(cx),
4481 [Point::new(2, 3)..Point::new(2, 3)]
4482 );
4483
4484 // We can remove trailing newlines
4485 editor.join_lines(&JoinLines, window, cx);
4486 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4487 assert_eq!(
4488 editor.selections.ranges::<Point>(cx),
4489 [Point::new(2, 3)..Point::new(2, 3)]
4490 );
4491
4492 // We don't blow up on the last line
4493 editor.join_lines(&JoinLines, window, cx);
4494 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4495 assert_eq!(
4496 editor.selections.ranges::<Point>(cx),
4497 [Point::new(2, 3)..Point::new(2, 3)]
4498 );
4499
4500 // reset to test indentation
4501 editor.buffer.update(cx, |buffer, cx| {
4502 buffer.edit(
4503 [
4504 (Point::new(1, 0)..Point::new(1, 2), " "),
4505 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4506 ],
4507 None,
4508 cx,
4509 )
4510 });
4511
4512 // We remove any leading spaces
4513 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4514 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4515 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4516 });
4517 editor.join_lines(&JoinLines, window, cx);
4518 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4519
4520 // We don't insert a space for a line containing only spaces
4521 editor.join_lines(&JoinLines, window, cx);
4522 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4523
4524 // We ignore any leading tabs
4525 editor.join_lines(&JoinLines, window, cx);
4526 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4527
4528 editor
4529 });
4530}
4531
4532#[gpui::test]
4533fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4534 init_test(cx, |_| {});
4535
4536 cx.add_window(|window, cx| {
4537 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4538 let mut editor = build_editor(buffer.clone(), window, cx);
4539 let buffer = buffer.read(cx).as_singleton().unwrap();
4540
4541 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4542 s.select_ranges([
4543 Point::new(0, 2)..Point::new(1, 1),
4544 Point::new(1, 2)..Point::new(1, 2),
4545 Point::new(3, 1)..Point::new(3, 2),
4546 ])
4547 });
4548
4549 editor.join_lines(&JoinLines, window, cx);
4550 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4551
4552 assert_eq!(
4553 editor.selections.ranges::<Point>(cx),
4554 [
4555 Point::new(0, 7)..Point::new(0, 7),
4556 Point::new(1, 3)..Point::new(1, 3)
4557 ]
4558 );
4559 editor
4560 });
4561}
4562
4563#[gpui::test]
4564async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4565 init_test(cx, |_| {});
4566
4567 let mut cx = EditorTestContext::new(cx).await;
4568
4569 let diff_base = r#"
4570 Line 0
4571 Line 1
4572 Line 2
4573 Line 3
4574 "#
4575 .unindent();
4576
4577 cx.set_state(
4578 &r#"
4579 ˇLine 0
4580 Line 1
4581 Line 2
4582 Line 3
4583 "#
4584 .unindent(),
4585 );
4586
4587 cx.set_head_text(&diff_base);
4588 executor.run_until_parked();
4589
4590 // Join lines
4591 cx.update_editor(|editor, window, cx| {
4592 editor.join_lines(&JoinLines, window, cx);
4593 });
4594 executor.run_until_parked();
4595
4596 cx.assert_editor_state(
4597 &r#"
4598 Line 0ˇ Line 1
4599 Line 2
4600 Line 3
4601 "#
4602 .unindent(),
4603 );
4604 // Join again
4605 cx.update_editor(|editor, window, cx| {
4606 editor.join_lines(&JoinLines, window, cx);
4607 });
4608 executor.run_until_parked();
4609
4610 cx.assert_editor_state(
4611 &r#"
4612 Line 0 Line 1ˇ Line 2
4613 Line 3
4614 "#
4615 .unindent(),
4616 );
4617}
4618
4619#[gpui::test]
4620async fn test_custom_newlines_cause_no_false_positive_diffs(
4621 executor: BackgroundExecutor,
4622 cx: &mut TestAppContext,
4623) {
4624 init_test(cx, |_| {});
4625 let mut cx = EditorTestContext::new(cx).await;
4626 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4627 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4628 executor.run_until_parked();
4629
4630 cx.update_editor(|editor, window, cx| {
4631 let snapshot = editor.snapshot(window, cx);
4632 assert_eq!(
4633 snapshot
4634 .buffer_snapshot()
4635 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
4636 .collect::<Vec<_>>(),
4637 Vec::new(),
4638 "Should not have any diffs for files with custom newlines"
4639 );
4640 });
4641}
4642
4643#[gpui::test]
4644async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4645 init_test(cx, |_| {});
4646
4647 let mut cx = EditorTestContext::new(cx).await;
4648
4649 // Test sort_lines_case_insensitive()
4650 cx.set_state(indoc! {"
4651 «z
4652 y
4653 x
4654 Z
4655 Y
4656 Xˇ»
4657 "});
4658 cx.update_editor(|e, window, cx| {
4659 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4660 });
4661 cx.assert_editor_state(indoc! {"
4662 «x
4663 X
4664 y
4665 Y
4666 z
4667 Zˇ»
4668 "});
4669
4670 // Test sort_lines_by_length()
4671 //
4672 // Demonstrates:
4673 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4674 // - sort is stable
4675 cx.set_state(indoc! {"
4676 «123
4677 æ
4678 12
4679 ∞
4680 1
4681 æˇ»
4682 "});
4683 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4684 cx.assert_editor_state(indoc! {"
4685 «æ
4686 ∞
4687 1
4688 æ
4689 12
4690 123ˇ»
4691 "});
4692
4693 // Test reverse_lines()
4694 cx.set_state(indoc! {"
4695 «5
4696 4
4697 3
4698 2
4699 1ˇ»
4700 "});
4701 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4702 cx.assert_editor_state(indoc! {"
4703 «1
4704 2
4705 3
4706 4
4707 5ˇ»
4708 "});
4709
4710 // Skip testing shuffle_line()
4711
4712 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4713 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4714
4715 // Don't manipulate when cursor is on single line, but expand the selection
4716 cx.set_state(indoc! {"
4717 ddˇdd
4718 ccc
4719 bb
4720 a
4721 "});
4722 cx.update_editor(|e, window, cx| {
4723 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4724 });
4725 cx.assert_editor_state(indoc! {"
4726 «ddddˇ»
4727 ccc
4728 bb
4729 a
4730 "});
4731
4732 // Basic manipulate case
4733 // Start selection moves to column 0
4734 // End of selection shrinks to fit shorter line
4735 cx.set_state(indoc! {"
4736 dd«d
4737 ccc
4738 bb
4739 aaaaaˇ»
4740 "});
4741 cx.update_editor(|e, window, cx| {
4742 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4743 });
4744 cx.assert_editor_state(indoc! {"
4745 «aaaaa
4746 bb
4747 ccc
4748 dddˇ»
4749 "});
4750
4751 // Manipulate case with newlines
4752 cx.set_state(indoc! {"
4753 dd«d
4754 ccc
4755
4756 bb
4757 aaaaa
4758
4759 ˇ»
4760 "});
4761 cx.update_editor(|e, window, cx| {
4762 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4763 });
4764 cx.assert_editor_state(indoc! {"
4765 «
4766
4767 aaaaa
4768 bb
4769 ccc
4770 dddˇ»
4771
4772 "});
4773
4774 // Adding new line
4775 cx.set_state(indoc! {"
4776 aa«a
4777 bbˇ»b
4778 "});
4779 cx.update_editor(|e, window, cx| {
4780 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4781 });
4782 cx.assert_editor_state(indoc! {"
4783 «aaa
4784 bbb
4785 added_lineˇ»
4786 "});
4787
4788 // Removing line
4789 cx.set_state(indoc! {"
4790 aa«a
4791 bbbˇ»
4792 "});
4793 cx.update_editor(|e, window, cx| {
4794 e.manipulate_immutable_lines(window, cx, |lines| {
4795 lines.pop();
4796 })
4797 });
4798 cx.assert_editor_state(indoc! {"
4799 «aaaˇ»
4800 "});
4801
4802 // Removing all lines
4803 cx.set_state(indoc! {"
4804 aa«a
4805 bbbˇ»
4806 "});
4807 cx.update_editor(|e, window, cx| {
4808 e.manipulate_immutable_lines(window, cx, |lines| {
4809 lines.drain(..);
4810 })
4811 });
4812 cx.assert_editor_state(indoc! {"
4813 ˇ
4814 "});
4815}
4816
4817#[gpui::test]
4818async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4819 init_test(cx, |_| {});
4820
4821 let mut cx = EditorTestContext::new(cx).await;
4822
4823 // Consider continuous selection as single selection
4824 cx.set_state(indoc! {"
4825 Aaa«aa
4826 cˇ»c«c
4827 bb
4828 aaaˇ»aa
4829 "});
4830 cx.update_editor(|e, window, cx| {
4831 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4832 });
4833 cx.assert_editor_state(indoc! {"
4834 «Aaaaa
4835 ccc
4836 bb
4837 aaaaaˇ»
4838 "});
4839
4840 cx.set_state(indoc! {"
4841 Aaa«aa
4842 cˇ»c«c
4843 bb
4844 aaaˇ»aa
4845 "});
4846 cx.update_editor(|e, window, cx| {
4847 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4848 });
4849 cx.assert_editor_state(indoc! {"
4850 «Aaaaa
4851 ccc
4852 bbˇ»
4853 "});
4854
4855 // Consider non continuous selection as distinct dedup operations
4856 cx.set_state(indoc! {"
4857 «aaaaa
4858 bb
4859 aaaaa
4860 aaaaaˇ»
4861
4862 aaa«aaˇ»
4863 "});
4864 cx.update_editor(|e, window, cx| {
4865 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4866 });
4867 cx.assert_editor_state(indoc! {"
4868 «aaaaa
4869 bbˇ»
4870
4871 «aaaaaˇ»
4872 "});
4873}
4874
4875#[gpui::test]
4876async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4877 init_test(cx, |_| {});
4878
4879 let mut cx = EditorTestContext::new(cx).await;
4880
4881 cx.set_state(indoc! {"
4882 «Aaa
4883 aAa
4884 Aaaˇ»
4885 "});
4886 cx.update_editor(|e, window, cx| {
4887 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4888 });
4889 cx.assert_editor_state(indoc! {"
4890 «Aaa
4891 aAaˇ»
4892 "});
4893
4894 cx.set_state(indoc! {"
4895 «Aaa
4896 aAa
4897 aaAˇ»
4898 "});
4899 cx.update_editor(|e, window, cx| {
4900 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4901 });
4902 cx.assert_editor_state(indoc! {"
4903 «Aaaˇ»
4904 "});
4905}
4906
4907#[gpui::test]
4908async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4909 init_test(cx, |_| {});
4910
4911 let mut cx = EditorTestContext::new(cx).await;
4912
4913 let js_language = Arc::new(Language::new(
4914 LanguageConfig {
4915 name: "JavaScript".into(),
4916 wrap_characters: Some(language::WrapCharactersConfig {
4917 start_prefix: "<".into(),
4918 start_suffix: ">".into(),
4919 end_prefix: "</".into(),
4920 end_suffix: ">".into(),
4921 }),
4922 ..LanguageConfig::default()
4923 },
4924 None,
4925 ));
4926
4927 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4928
4929 cx.set_state(indoc! {"
4930 «testˇ»
4931 "});
4932 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4933 cx.assert_editor_state(indoc! {"
4934 <«ˇ»>test</«ˇ»>
4935 "});
4936
4937 cx.set_state(indoc! {"
4938 «test
4939 testˇ»
4940 "});
4941 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4942 cx.assert_editor_state(indoc! {"
4943 <«ˇ»>test
4944 test</«ˇ»>
4945 "});
4946
4947 cx.set_state(indoc! {"
4948 teˇst
4949 "});
4950 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4951 cx.assert_editor_state(indoc! {"
4952 te<«ˇ»></«ˇ»>st
4953 "});
4954}
4955
4956#[gpui::test]
4957async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
4958 init_test(cx, |_| {});
4959
4960 let mut cx = EditorTestContext::new(cx).await;
4961
4962 let js_language = Arc::new(Language::new(
4963 LanguageConfig {
4964 name: "JavaScript".into(),
4965 wrap_characters: Some(language::WrapCharactersConfig {
4966 start_prefix: "<".into(),
4967 start_suffix: ">".into(),
4968 end_prefix: "</".into(),
4969 end_suffix: ">".into(),
4970 }),
4971 ..LanguageConfig::default()
4972 },
4973 None,
4974 ));
4975
4976 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4977
4978 cx.set_state(indoc! {"
4979 «testˇ»
4980 «testˇ» «testˇ»
4981 «testˇ»
4982 "});
4983 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4984 cx.assert_editor_state(indoc! {"
4985 <«ˇ»>test</«ˇ»>
4986 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
4987 <«ˇ»>test</«ˇ»>
4988 "});
4989
4990 cx.set_state(indoc! {"
4991 «test
4992 testˇ»
4993 «test
4994 testˇ»
4995 "});
4996 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4997 cx.assert_editor_state(indoc! {"
4998 <«ˇ»>test
4999 test</«ˇ»>
5000 <«ˇ»>test
5001 test</«ˇ»>
5002 "});
5003}
5004
5005#[gpui::test]
5006async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5007 init_test(cx, |_| {});
5008
5009 let mut cx = EditorTestContext::new(cx).await;
5010
5011 let plaintext_language = Arc::new(Language::new(
5012 LanguageConfig {
5013 name: "Plain Text".into(),
5014 ..LanguageConfig::default()
5015 },
5016 None,
5017 ));
5018
5019 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5020
5021 cx.set_state(indoc! {"
5022 «testˇ»
5023 "});
5024 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5025 cx.assert_editor_state(indoc! {"
5026 «testˇ»
5027 "});
5028}
5029
5030#[gpui::test]
5031async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5032 init_test(cx, |_| {});
5033
5034 let mut cx = EditorTestContext::new(cx).await;
5035
5036 // Manipulate with multiple selections on a single line
5037 cx.set_state(indoc! {"
5038 dd«dd
5039 cˇ»c«c
5040 bb
5041 aaaˇ»aa
5042 "});
5043 cx.update_editor(|e, window, cx| {
5044 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5045 });
5046 cx.assert_editor_state(indoc! {"
5047 «aaaaa
5048 bb
5049 ccc
5050 ddddˇ»
5051 "});
5052
5053 // Manipulate with multiple disjoin selections
5054 cx.set_state(indoc! {"
5055 5«
5056 4
5057 3
5058 2
5059 1ˇ»
5060
5061 dd«dd
5062 ccc
5063 bb
5064 aaaˇ»aa
5065 "});
5066 cx.update_editor(|e, window, cx| {
5067 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5068 });
5069 cx.assert_editor_state(indoc! {"
5070 «1
5071 2
5072 3
5073 4
5074 5ˇ»
5075
5076 «aaaaa
5077 bb
5078 ccc
5079 ddddˇ»
5080 "});
5081
5082 // Adding lines on each selection
5083 cx.set_state(indoc! {"
5084 2«
5085 1ˇ»
5086
5087 bb«bb
5088 aaaˇ»aa
5089 "});
5090 cx.update_editor(|e, window, cx| {
5091 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5092 });
5093 cx.assert_editor_state(indoc! {"
5094 «2
5095 1
5096 added lineˇ»
5097
5098 «bbbb
5099 aaaaa
5100 added lineˇ»
5101 "});
5102
5103 // Removing lines on each selection
5104 cx.set_state(indoc! {"
5105 2«
5106 1ˇ»
5107
5108 bb«bb
5109 aaaˇ»aa
5110 "});
5111 cx.update_editor(|e, window, cx| {
5112 e.manipulate_immutable_lines(window, cx, |lines| {
5113 lines.pop();
5114 })
5115 });
5116 cx.assert_editor_state(indoc! {"
5117 «2ˇ»
5118
5119 «bbbbˇ»
5120 "});
5121}
5122
5123#[gpui::test]
5124async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5125 init_test(cx, |settings| {
5126 settings.defaults.tab_size = NonZeroU32::new(3)
5127 });
5128
5129 let mut cx = EditorTestContext::new(cx).await;
5130
5131 // MULTI SELECTION
5132 // Ln.1 "«" tests empty lines
5133 // Ln.9 tests just leading whitespace
5134 cx.set_state(indoc! {"
5135 «
5136 abc // No indentationˇ»
5137 «\tabc // 1 tabˇ»
5138 \t\tabc « ˇ» // 2 tabs
5139 \t ab«c // Tab followed by space
5140 \tabc // Space followed by tab (3 spaces should be the result)
5141 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5142 abˇ»ˇc ˇ ˇ // Already space indented«
5143 \t
5144 \tabc\tdef // Only the leading tab is manipulatedˇ»
5145 "});
5146 cx.update_editor(|e, window, cx| {
5147 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5148 });
5149 cx.assert_editor_state(
5150 indoc! {"
5151 «
5152 abc // No indentation
5153 abc // 1 tab
5154 abc // 2 tabs
5155 abc // Tab followed by space
5156 abc // Space followed by tab (3 spaces should be the result)
5157 abc // Mixed indentation (tab conversion depends on the column)
5158 abc // Already space indented
5159 ·
5160 abc\tdef // Only the leading tab is manipulatedˇ»
5161 "}
5162 .replace("·", "")
5163 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5164 );
5165
5166 // Test on just a few lines, the others should remain unchanged
5167 // Only lines (3, 5, 10, 11) should change
5168 cx.set_state(
5169 indoc! {"
5170 ·
5171 abc // No indentation
5172 \tabcˇ // 1 tab
5173 \t\tabc // 2 tabs
5174 \t abcˇ // Tab followed by space
5175 \tabc // Space followed by tab (3 spaces should be the result)
5176 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5177 abc // Already space indented
5178 «\t
5179 \tabc\tdef // Only the leading tab is manipulatedˇ»
5180 "}
5181 .replace("·", "")
5182 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5183 );
5184 cx.update_editor(|e, window, cx| {
5185 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5186 });
5187 cx.assert_editor_state(
5188 indoc! {"
5189 ·
5190 abc // No indentation
5191 « abc // 1 tabˇ»
5192 \t\tabc // 2 tabs
5193 « abc // Tab followed by spaceˇ»
5194 \tabc // Space followed by tab (3 spaces should be the result)
5195 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5196 abc // Already space indented
5197 « ·
5198 abc\tdef // Only the leading tab is manipulatedˇ»
5199 "}
5200 .replace("·", "")
5201 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5202 );
5203
5204 // SINGLE SELECTION
5205 // Ln.1 "«" tests empty lines
5206 // Ln.9 tests just leading whitespace
5207 cx.set_state(indoc! {"
5208 «
5209 abc // No indentation
5210 \tabc // 1 tab
5211 \t\tabc // 2 tabs
5212 \t abc // Tab followed by space
5213 \tabc // Space followed by tab (3 spaces should be the result)
5214 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5215 abc // Already space indented
5216 \t
5217 \tabc\tdef // Only the leading tab is manipulatedˇ»
5218 "});
5219 cx.update_editor(|e, window, cx| {
5220 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5221 });
5222 cx.assert_editor_state(
5223 indoc! {"
5224 «
5225 abc // No indentation
5226 abc // 1 tab
5227 abc // 2 tabs
5228 abc // Tab followed by space
5229 abc // Space followed by tab (3 spaces should be the result)
5230 abc // Mixed indentation (tab conversion depends on the column)
5231 abc // Already space indented
5232 ·
5233 abc\tdef // Only the leading tab is manipulatedˇ»
5234 "}
5235 .replace("·", "")
5236 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5237 );
5238}
5239
5240#[gpui::test]
5241async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5242 init_test(cx, |settings| {
5243 settings.defaults.tab_size = NonZeroU32::new(3)
5244 });
5245
5246 let mut cx = EditorTestContext::new(cx).await;
5247
5248 // MULTI SELECTION
5249 // Ln.1 "«" tests empty lines
5250 // Ln.11 tests just leading whitespace
5251 cx.set_state(indoc! {"
5252 «
5253 abˇ»ˇc // No indentation
5254 abc ˇ ˇ // 1 space (< 3 so dont convert)
5255 abc « // 2 spaces (< 3 so dont convert)
5256 abc // 3 spaces (convert)
5257 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5258 «\tˇ»\t«\tˇ»abc // Already tab indented
5259 «\t abc // Tab followed by space
5260 \tabc // Space followed by tab (should be consumed due to tab)
5261 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5262 \tˇ» «\t
5263 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5264 "});
5265 cx.update_editor(|e, window, cx| {
5266 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5267 });
5268 cx.assert_editor_state(indoc! {"
5269 «
5270 abc // No indentation
5271 abc // 1 space (< 3 so dont convert)
5272 abc // 2 spaces (< 3 so dont convert)
5273 \tabc // 3 spaces (convert)
5274 \t abc // 5 spaces (1 tab + 2 spaces)
5275 \t\t\tabc // Already tab indented
5276 \t abc // Tab followed by space
5277 \tabc // Space followed by tab (should be consumed due to tab)
5278 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5279 \t\t\t
5280 \tabc \t // Only the leading spaces should be convertedˇ»
5281 "});
5282
5283 // Test on just a few lines, the other should remain unchanged
5284 // Only lines (4, 8, 11, 12) should change
5285 cx.set_state(
5286 indoc! {"
5287 ·
5288 abc // No indentation
5289 abc // 1 space (< 3 so dont convert)
5290 abc // 2 spaces (< 3 so dont convert)
5291 « abc // 3 spaces (convert)ˇ»
5292 abc // 5 spaces (1 tab + 2 spaces)
5293 \t\t\tabc // Already tab indented
5294 \t abc // Tab followed by space
5295 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5296 \t\t \tabc // Mixed indentation
5297 \t \t \t \tabc // Mixed indentation
5298 \t \tˇ
5299 « abc \t // Only the leading spaces should be convertedˇ»
5300 "}
5301 .replace("·", "")
5302 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5303 );
5304 cx.update_editor(|e, window, cx| {
5305 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5306 });
5307 cx.assert_editor_state(
5308 indoc! {"
5309 ·
5310 abc // No indentation
5311 abc // 1 space (< 3 so dont convert)
5312 abc // 2 spaces (< 3 so dont convert)
5313 «\tabc // 3 spaces (convert)ˇ»
5314 abc // 5 spaces (1 tab + 2 spaces)
5315 \t\t\tabc // Already tab indented
5316 \t abc // Tab followed by space
5317 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5318 \t\t \tabc // Mixed indentation
5319 \t \t \t \tabc // Mixed indentation
5320 «\t\t\t
5321 \tabc \t // Only the leading spaces should be convertedˇ»
5322 "}
5323 .replace("·", "")
5324 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5325 );
5326
5327 // SINGLE SELECTION
5328 // Ln.1 "«" tests empty lines
5329 // Ln.11 tests just leading whitespace
5330 cx.set_state(indoc! {"
5331 «
5332 abc // No indentation
5333 abc // 1 space (< 3 so dont convert)
5334 abc // 2 spaces (< 3 so dont convert)
5335 abc // 3 spaces (convert)
5336 abc // 5 spaces (1 tab + 2 spaces)
5337 \t\t\tabc // Already tab indented
5338 \t abc // Tab followed by space
5339 \tabc // Space followed by tab (should be consumed due to tab)
5340 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5341 \t \t
5342 abc \t // Only the leading spaces should be convertedˇ»
5343 "});
5344 cx.update_editor(|e, window, cx| {
5345 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5346 });
5347 cx.assert_editor_state(indoc! {"
5348 «
5349 abc // No indentation
5350 abc // 1 space (< 3 so dont convert)
5351 abc // 2 spaces (< 3 so dont convert)
5352 \tabc // 3 spaces (convert)
5353 \t abc // 5 spaces (1 tab + 2 spaces)
5354 \t\t\tabc // Already tab indented
5355 \t abc // Tab followed by space
5356 \tabc // Space followed by tab (should be consumed due to tab)
5357 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5358 \t\t\t
5359 \tabc \t // Only the leading spaces should be convertedˇ»
5360 "});
5361}
5362
5363#[gpui::test]
5364async fn test_toggle_case(cx: &mut TestAppContext) {
5365 init_test(cx, |_| {});
5366
5367 let mut cx = EditorTestContext::new(cx).await;
5368
5369 // If all lower case -> upper case
5370 cx.set_state(indoc! {"
5371 «hello worldˇ»
5372 "});
5373 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5374 cx.assert_editor_state(indoc! {"
5375 «HELLO WORLDˇ»
5376 "});
5377
5378 // If all upper case -> lower case
5379 cx.set_state(indoc! {"
5380 «HELLO WORLDˇ»
5381 "});
5382 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5383 cx.assert_editor_state(indoc! {"
5384 «hello worldˇ»
5385 "});
5386
5387 // If any upper case characters are identified -> lower case
5388 // This matches JetBrains IDEs
5389 cx.set_state(indoc! {"
5390 «hEllo worldˇ»
5391 "});
5392 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5393 cx.assert_editor_state(indoc! {"
5394 «hello worldˇ»
5395 "});
5396}
5397
5398#[gpui::test]
5399async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5400 init_test(cx, |_| {});
5401
5402 let mut cx = EditorTestContext::new(cx).await;
5403
5404 cx.set_state(indoc! {"
5405 «implement-windows-supportˇ»
5406 "});
5407 cx.update_editor(|e, window, cx| {
5408 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5409 });
5410 cx.assert_editor_state(indoc! {"
5411 «Implement windows supportˇ»
5412 "});
5413}
5414
5415#[gpui::test]
5416async fn test_manipulate_text(cx: &mut TestAppContext) {
5417 init_test(cx, |_| {});
5418
5419 let mut cx = EditorTestContext::new(cx).await;
5420
5421 // Test convert_to_upper_case()
5422 cx.set_state(indoc! {"
5423 «hello worldˇ»
5424 "});
5425 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5426 cx.assert_editor_state(indoc! {"
5427 «HELLO WORLDˇ»
5428 "});
5429
5430 // Test convert_to_lower_case()
5431 cx.set_state(indoc! {"
5432 «HELLO WORLDˇ»
5433 "});
5434 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5435 cx.assert_editor_state(indoc! {"
5436 «hello worldˇ»
5437 "});
5438
5439 // Test multiple line, single selection case
5440 cx.set_state(indoc! {"
5441 «The quick brown
5442 fox jumps over
5443 the lazy dogˇ»
5444 "});
5445 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5446 cx.assert_editor_state(indoc! {"
5447 «The Quick Brown
5448 Fox Jumps Over
5449 The Lazy Dogˇ»
5450 "});
5451
5452 // Test multiple line, single selection case
5453 cx.set_state(indoc! {"
5454 «The quick brown
5455 fox jumps over
5456 the lazy dogˇ»
5457 "});
5458 cx.update_editor(|e, window, cx| {
5459 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5460 });
5461 cx.assert_editor_state(indoc! {"
5462 «TheQuickBrown
5463 FoxJumpsOver
5464 TheLazyDogˇ»
5465 "});
5466
5467 // From here on out, test more complex cases of manipulate_text()
5468
5469 // Test no selection case - should affect words cursors are in
5470 // Cursor at beginning, middle, and end of word
5471 cx.set_state(indoc! {"
5472 ˇhello big beauˇtiful worldˇ
5473 "});
5474 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5475 cx.assert_editor_state(indoc! {"
5476 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5477 "});
5478
5479 // Test multiple selections on a single line and across multiple lines
5480 cx.set_state(indoc! {"
5481 «Theˇ» quick «brown
5482 foxˇ» jumps «overˇ»
5483 the «lazyˇ» dog
5484 "});
5485 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5486 cx.assert_editor_state(indoc! {"
5487 «THEˇ» quick «BROWN
5488 FOXˇ» jumps «OVERˇ»
5489 the «LAZYˇ» dog
5490 "});
5491
5492 // Test case where text length grows
5493 cx.set_state(indoc! {"
5494 «tschüߡ»
5495 "});
5496 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5497 cx.assert_editor_state(indoc! {"
5498 «TSCHÜSSˇ»
5499 "});
5500
5501 // Test to make sure we don't crash when text shrinks
5502 cx.set_state(indoc! {"
5503 aaa_bbbˇ
5504 "});
5505 cx.update_editor(|e, window, cx| {
5506 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5507 });
5508 cx.assert_editor_state(indoc! {"
5509 «aaaBbbˇ»
5510 "});
5511
5512 // Test to make sure we all aware of the fact that each word can grow and shrink
5513 // Final selections should be aware of this fact
5514 cx.set_state(indoc! {"
5515 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5516 "});
5517 cx.update_editor(|e, window, cx| {
5518 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5519 });
5520 cx.assert_editor_state(indoc! {"
5521 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5522 "});
5523
5524 cx.set_state(indoc! {"
5525 «hElLo, WoRld!ˇ»
5526 "});
5527 cx.update_editor(|e, window, cx| {
5528 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5529 });
5530 cx.assert_editor_state(indoc! {"
5531 «HeLlO, wOrLD!ˇ»
5532 "});
5533
5534 // Test selections with `line_mode() = true`.
5535 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5536 cx.set_state(indoc! {"
5537 «The quick brown
5538 fox jumps over
5539 tˇ»he lazy dog
5540 "});
5541 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5542 cx.assert_editor_state(indoc! {"
5543 «THE QUICK BROWN
5544 FOX JUMPS OVER
5545 THE LAZY DOGˇ»
5546 "});
5547}
5548
5549#[gpui::test]
5550fn test_duplicate_line(cx: &mut TestAppContext) {
5551 init_test(cx, |_| {});
5552
5553 let editor = cx.add_window(|window, cx| {
5554 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5555 build_editor(buffer, window, cx)
5556 });
5557 _ = editor.update(cx, |editor, window, cx| {
5558 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5559 s.select_display_ranges([
5560 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5561 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5562 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5563 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5564 ])
5565 });
5566 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5567 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5568 assert_eq!(
5569 editor.selections.display_ranges(cx),
5570 vec![
5571 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5572 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5573 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5574 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5575 ]
5576 );
5577 });
5578
5579 let editor = cx.add_window(|window, cx| {
5580 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5581 build_editor(buffer, window, cx)
5582 });
5583 _ = editor.update(cx, |editor, window, cx| {
5584 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5585 s.select_display_ranges([
5586 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5587 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5588 ])
5589 });
5590 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5591 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5592 assert_eq!(
5593 editor.selections.display_ranges(cx),
5594 vec![
5595 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5596 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5597 ]
5598 );
5599 });
5600
5601 // With `move_upwards` the selections stay in place, except for
5602 // the lines inserted above them
5603 let editor = cx.add_window(|window, cx| {
5604 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5605 build_editor(buffer, window, cx)
5606 });
5607 _ = editor.update(cx, |editor, window, cx| {
5608 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5609 s.select_display_ranges([
5610 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5611 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5612 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5613 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5614 ])
5615 });
5616 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5617 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5618 assert_eq!(
5619 editor.selections.display_ranges(cx),
5620 vec![
5621 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5622 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5623 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5624 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5625 ]
5626 );
5627 });
5628
5629 let editor = cx.add_window(|window, cx| {
5630 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5631 build_editor(buffer, window, cx)
5632 });
5633 _ = editor.update(cx, |editor, window, cx| {
5634 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5635 s.select_display_ranges([
5636 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5637 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5638 ])
5639 });
5640 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5641 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5642 assert_eq!(
5643 editor.selections.display_ranges(cx),
5644 vec![
5645 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5646 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5647 ]
5648 );
5649 });
5650
5651 let editor = cx.add_window(|window, cx| {
5652 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5653 build_editor(buffer, window, cx)
5654 });
5655 _ = editor.update(cx, |editor, window, cx| {
5656 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5657 s.select_display_ranges([
5658 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5659 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5660 ])
5661 });
5662 editor.duplicate_selection(&DuplicateSelection, window, cx);
5663 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5664 assert_eq!(
5665 editor.selections.display_ranges(cx),
5666 vec![
5667 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5668 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5669 ]
5670 );
5671 });
5672}
5673
5674#[gpui::test]
5675fn test_move_line_up_down(cx: &mut TestAppContext) {
5676 init_test(cx, |_| {});
5677
5678 let editor = cx.add_window(|window, cx| {
5679 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5680 build_editor(buffer, window, cx)
5681 });
5682 _ = editor.update(cx, |editor, window, cx| {
5683 editor.fold_creases(
5684 vec![
5685 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5686 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5687 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5688 ],
5689 true,
5690 window,
5691 cx,
5692 );
5693 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5694 s.select_display_ranges([
5695 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5696 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5697 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5698 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5699 ])
5700 });
5701 assert_eq!(
5702 editor.display_text(cx),
5703 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5704 );
5705
5706 editor.move_line_up(&MoveLineUp, window, cx);
5707 assert_eq!(
5708 editor.display_text(cx),
5709 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5710 );
5711 assert_eq!(
5712 editor.selections.display_ranges(cx),
5713 vec![
5714 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5715 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5716 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5717 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5718 ]
5719 );
5720 });
5721
5722 _ = editor.update(cx, |editor, window, cx| {
5723 editor.move_line_down(&MoveLineDown, window, cx);
5724 assert_eq!(
5725 editor.display_text(cx),
5726 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5727 );
5728 assert_eq!(
5729 editor.selections.display_ranges(cx),
5730 vec![
5731 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5732 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5733 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5734 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5735 ]
5736 );
5737 });
5738
5739 _ = editor.update(cx, |editor, window, cx| {
5740 editor.move_line_down(&MoveLineDown, window, cx);
5741 assert_eq!(
5742 editor.display_text(cx),
5743 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5744 );
5745 assert_eq!(
5746 editor.selections.display_ranges(cx),
5747 vec![
5748 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5749 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5750 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5751 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5752 ]
5753 );
5754 });
5755
5756 _ = editor.update(cx, |editor, window, cx| {
5757 editor.move_line_up(&MoveLineUp, window, cx);
5758 assert_eq!(
5759 editor.display_text(cx),
5760 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5761 );
5762 assert_eq!(
5763 editor.selections.display_ranges(cx),
5764 vec![
5765 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5766 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5767 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5768 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5769 ]
5770 );
5771 });
5772}
5773
5774#[gpui::test]
5775fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5776 init_test(cx, |_| {});
5777 let editor = cx.add_window(|window, cx| {
5778 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5779 build_editor(buffer, window, cx)
5780 });
5781 _ = editor.update(cx, |editor, window, cx| {
5782 editor.fold_creases(
5783 vec![Crease::simple(
5784 Point::new(6, 4)..Point::new(7, 4),
5785 FoldPlaceholder::test(),
5786 )],
5787 true,
5788 window,
5789 cx,
5790 );
5791 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5792 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5793 });
5794 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5795 editor.move_line_up(&MoveLineUp, window, cx);
5796 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5797 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5798 });
5799}
5800
5801#[gpui::test]
5802fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5803 init_test(cx, |_| {});
5804
5805 let editor = cx.add_window(|window, cx| {
5806 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5807 build_editor(buffer, window, cx)
5808 });
5809 _ = editor.update(cx, |editor, window, cx| {
5810 let snapshot = editor.buffer.read(cx).snapshot(cx);
5811 editor.insert_blocks(
5812 [BlockProperties {
5813 style: BlockStyle::Fixed,
5814 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5815 height: Some(1),
5816 render: Arc::new(|_| div().into_any()),
5817 priority: 0,
5818 }],
5819 Some(Autoscroll::fit()),
5820 cx,
5821 );
5822 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5823 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5824 });
5825 editor.move_line_down(&MoveLineDown, window, cx);
5826 });
5827}
5828
5829#[gpui::test]
5830async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5831 init_test(cx, |_| {});
5832
5833 let mut cx = EditorTestContext::new(cx).await;
5834 cx.set_state(
5835 &"
5836 ˇzero
5837 one
5838 two
5839 three
5840 four
5841 five
5842 "
5843 .unindent(),
5844 );
5845
5846 // Create a four-line block that replaces three lines of text.
5847 cx.update_editor(|editor, window, cx| {
5848 let snapshot = editor.snapshot(window, cx);
5849 let snapshot = &snapshot.buffer_snapshot();
5850 let placement = BlockPlacement::Replace(
5851 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5852 );
5853 editor.insert_blocks(
5854 [BlockProperties {
5855 placement,
5856 height: Some(4),
5857 style: BlockStyle::Sticky,
5858 render: Arc::new(|_| gpui::div().into_any_element()),
5859 priority: 0,
5860 }],
5861 None,
5862 cx,
5863 );
5864 });
5865
5866 // Move down so that the cursor touches the block.
5867 cx.update_editor(|editor, window, cx| {
5868 editor.move_down(&Default::default(), window, cx);
5869 });
5870 cx.assert_editor_state(
5871 &"
5872 zero
5873 «one
5874 two
5875 threeˇ»
5876 four
5877 five
5878 "
5879 .unindent(),
5880 );
5881
5882 // Move down past the block.
5883 cx.update_editor(|editor, window, cx| {
5884 editor.move_down(&Default::default(), window, cx);
5885 });
5886 cx.assert_editor_state(
5887 &"
5888 zero
5889 one
5890 two
5891 three
5892 ˇfour
5893 five
5894 "
5895 .unindent(),
5896 );
5897}
5898
5899#[gpui::test]
5900fn test_transpose(cx: &mut TestAppContext) {
5901 init_test(cx, |_| {});
5902
5903 _ = cx.add_window(|window, cx| {
5904 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5905 editor.set_style(EditorStyle::default(), window, cx);
5906 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5907 s.select_ranges([1..1])
5908 });
5909 editor.transpose(&Default::default(), window, cx);
5910 assert_eq!(editor.text(cx), "bac");
5911 assert_eq!(editor.selections.ranges(cx), [2..2]);
5912
5913 editor.transpose(&Default::default(), window, cx);
5914 assert_eq!(editor.text(cx), "bca");
5915 assert_eq!(editor.selections.ranges(cx), [3..3]);
5916
5917 editor.transpose(&Default::default(), window, cx);
5918 assert_eq!(editor.text(cx), "bac");
5919 assert_eq!(editor.selections.ranges(cx), [3..3]);
5920
5921 editor
5922 });
5923
5924 _ = cx.add_window(|window, cx| {
5925 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5926 editor.set_style(EditorStyle::default(), window, cx);
5927 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5928 s.select_ranges([3..3])
5929 });
5930 editor.transpose(&Default::default(), window, cx);
5931 assert_eq!(editor.text(cx), "acb\nde");
5932 assert_eq!(editor.selections.ranges(cx), [3..3]);
5933
5934 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5935 s.select_ranges([4..4])
5936 });
5937 editor.transpose(&Default::default(), window, cx);
5938 assert_eq!(editor.text(cx), "acbd\ne");
5939 assert_eq!(editor.selections.ranges(cx), [5..5]);
5940
5941 editor.transpose(&Default::default(), window, cx);
5942 assert_eq!(editor.text(cx), "acbde\n");
5943 assert_eq!(editor.selections.ranges(cx), [6..6]);
5944
5945 editor.transpose(&Default::default(), window, cx);
5946 assert_eq!(editor.text(cx), "acbd\ne");
5947 assert_eq!(editor.selections.ranges(cx), [6..6]);
5948
5949 editor
5950 });
5951
5952 _ = cx.add_window(|window, cx| {
5953 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5954 editor.set_style(EditorStyle::default(), window, cx);
5955 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5956 s.select_ranges([1..1, 2..2, 4..4])
5957 });
5958 editor.transpose(&Default::default(), window, cx);
5959 assert_eq!(editor.text(cx), "bacd\ne");
5960 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5961
5962 editor.transpose(&Default::default(), window, cx);
5963 assert_eq!(editor.text(cx), "bcade\n");
5964 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5965
5966 editor.transpose(&Default::default(), window, cx);
5967 assert_eq!(editor.text(cx), "bcda\ne");
5968 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5969
5970 editor.transpose(&Default::default(), window, cx);
5971 assert_eq!(editor.text(cx), "bcade\n");
5972 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5973
5974 editor.transpose(&Default::default(), window, cx);
5975 assert_eq!(editor.text(cx), "bcaed\n");
5976 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5977
5978 editor
5979 });
5980
5981 _ = cx.add_window(|window, cx| {
5982 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5983 editor.set_style(EditorStyle::default(), window, cx);
5984 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5985 s.select_ranges([4..4])
5986 });
5987 editor.transpose(&Default::default(), window, cx);
5988 assert_eq!(editor.text(cx), "🏀🍐✋");
5989 assert_eq!(editor.selections.ranges(cx), [8..8]);
5990
5991 editor.transpose(&Default::default(), window, cx);
5992 assert_eq!(editor.text(cx), "🏀✋🍐");
5993 assert_eq!(editor.selections.ranges(cx), [11..11]);
5994
5995 editor.transpose(&Default::default(), window, cx);
5996 assert_eq!(editor.text(cx), "🏀🍐✋");
5997 assert_eq!(editor.selections.ranges(cx), [11..11]);
5998
5999 editor
6000 });
6001}
6002
6003#[gpui::test]
6004async fn test_rewrap(cx: &mut TestAppContext) {
6005 init_test(cx, |settings| {
6006 settings.languages.0.extend([
6007 (
6008 "Markdown".into(),
6009 LanguageSettingsContent {
6010 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6011 preferred_line_length: Some(40),
6012 ..Default::default()
6013 },
6014 ),
6015 (
6016 "Plain Text".into(),
6017 LanguageSettingsContent {
6018 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6019 preferred_line_length: Some(40),
6020 ..Default::default()
6021 },
6022 ),
6023 (
6024 "C++".into(),
6025 LanguageSettingsContent {
6026 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6027 preferred_line_length: Some(40),
6028 ..Default::default()
6029 },
6030 ),
6031 (
6032 "Python".into(),
6033 LanguageSettingsContent {
6034 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6035 preferred_line_length: Some(40),
6036 ..Default::default()
6037 },
6038 ),
6039 (
6040 "Rust".into(),
6041 LanguageSettingsContent {
6042 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6043 preferred_line_length: Some(40),
6044 ..Default::default()
6045 },
6046 ),
6047 ])
6048 });
6049
6050 let mut cx = EditorTestContext::new(cx).await;
6051
6052 let cpp_language = Arc::new(Language::new(
6053 LanguageConfig {
6054 name: "C++".into(),
6055 line_comments: vec!["// ".into()],
6056 ..LanguageConfig::default()
6057 },
6058 None,
6059 ));
6060 let python_language = Arc::new(Language::new(
6061 LanguageConfig {
6062 name: "Python".into(),
6063 line_comments: vec!["# ".into()],
6064 ..LanguageConfig::default()
6065 },
6066 None,
6067 ));
6068 let markdown_language = Arc::new(Language::new(
6069 LanguageConfig {
6070 name: "Markdown".into(),
6071 rewrap_prefixes: vec![
6072 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6073 regex::Regex::new("[-*+]\\s+").unwrap(),
6074 ],
6075 ..LanguageConfig::default()
6076 },
6077 None,
6078 ));
6079 let rust_language = Arc::new(
6080 Language::new(
6081 LanguageConfig {
6082 name: "Rust".into(),
6083 line_comments: vec!["// ".into(), "/// ".into()],
6084 ..LanguageConfig::default()
6085 },
6086 Some(tree_sitter_rust::LANGUAGE.into()),
6087 )
6088 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6089 .unwrap(),
6090 );
6091
6092 let plaintext_language = Arc::new(Language::new(
6093 LanguageConfig {
6094 name: "Plain Text".into(),
6095 ..LanguageConfig::default()
6096 },
6097 None,
6098 ));
6099
6100 // Test basic rewrapping of a long line with a cursor
6101 assert_rewrap(
6102 indoc! {"
6103 // ˇThis is a long comment that needs to be wrapped.
6104 "},
6105 indoc! {"
6106 // ˇThis is a long comment that needs to
6107 // be wrapped.
6108 "},
6109 cpp_language.clone(),
6110 &mut cx,
6111 );
6112
6113 // Test rewrapping a full selection
6114 assert_rewrap(
6115 indoc! {"
6116 «// This selected long comment needs to be wrapped.ˇ»"
6117 },
6118 indoc! {"
6119 «// This selected long comment needs to
6120 // be wrapped.ˇ»"
6121 },
6122 cpp_language.clone(),
6123 &mut cx,
6124 );
6125
6126 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6127 assert_rewrap(
6128 indoc! {"
6129 // ˇThis is the first line.
6130 // Thisˇ is the second line.
6131 // This is the thirdˇ line, all part of one paragraph.
6132 "},
6133 indoc! {"
6134 // ˇThis is the first line. Thisˇ is the
6135 // second line. This is the thirdˇ line,
6136 // all part of one paragraph.
6137 "},
6138 cpp_language.clone(),
6139 &mut cx,
6140 );
6141
6142 // Test multiple cursors in different paragraphs trigger separate rewraps
6143 assert_rewrap(
6144 indoc! {"
6145 // ˇThis is the first paragraph, first line.
6146 // ˇThis is the first paragraph, second line.
6147
6148 // ˇThis is the second paragraph, first line.
6149 // ˇThis is the second paragraph, second line.
6150 "},
6151 indoc! {"
6152 // ˇThis is the first paragraph, first
6153 // line. ˇThis is the first paragraph,
6154 // second line.
6155
6156 // ˇThis is the second paragraph, first
6157 // line. ˇThis is the second paragraph,
6158 // second line.
6159 "},
6160 cpp_language.clone(),
6161 &mut cx,
6162 );
6163
6164 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6165 assert_rewrap(
6166 indoc! {"
6167 «// A regular long long comment to be wrapped.
6168 /// A documentation long comment to be wrapped.ˇ»
6169 "},
6170 indoc! {"
6171 «// A regular long long comment to be
6172 // wrapped.
6173 /// A documentation long comment to be
6174 /// wrapped.ˇ»
6175 "},
6176 rust_language.clone(),
6177 &mut cx,
6178 );
6179
6180 // Test that change in indentation level trigger seperate rewraps
6181 assert_rewrap(
6182 indoc! {"
6183 fn foo() {
6184 «// This is a long comment at the base indent.
6185 // This is a long comment at the next indent.ˇ»
6186 }
6187 "},
6188 indoc! {"
6189 fn foo() {
6190 «// This is a long comment at the
6191 // base indent.
6192 // This is a long comment at the
6193 // next indent.ˇ»
6194 }
6195 "},
6196 rust_language.clone(),
6197 &mut cx,
6198 );
6199
6200 // Test that different comment prefix characters (e.g., '#') are handled correctly
6201 assert_rewrap(
6202 indoc! {"
6203 # ˇThis is a long comment using a pound sign.
6204 "},
6205 indoc! {"
6206 # ˇThis is a long comment using a pound
6207 # sign.
6208 "},
6209 python_language,
6210 &mut cx,
6211 );
6212
6213 // Test rewrapping only affects comments, not code even when selected
6214 assert_rewrap(
6215 indoc! {"
6216 «/// This doc comment is long and should be wrapped.
6217 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6218 "},
6219 indoc! {"
6220 «/// This doc comment is long and should
6221 /// be wrapped.
6222 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6223 "},
6224 rust_language.clone(),
6225 &mut cx,
6226 );
6227
6228 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6229 assert_rewrap(
6230 indoc! {"
6231 # Header
6232
6233 A long long long line of markdown text to wrap.ˇ
6234 "},
6235 indoc! {"
6236 # Header
6237
6238 A long long long line of markdown text
6239 to wrap.ˇ
6240 "},
6241 markdown_language.clone(),
6242 &mut cx,
6243 );
6244
6245 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6246 assert_rewrap(
6247 indoc! {"
6248 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6249 2. This is a numbered list item that is very long and needs to be wrapped properly.
6250 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6251 "},
6252 indoc! {"
6253 «1. This is a numbered list item that is
6254 very long and needs to be wrapped
6255 properly.
6256 2. This is a numbered list item that is
6257 very long and needs to be wrapped
6258 properly.
6259 - This is an unordered list item that is
6260 also very long and should not merge
6261 with the numbered item.ˇ»
6262 "},
6263 markdown_language.clone(),
6264 &mut cx,
6265 );
6266
6267 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6268 assert_rewrap(
6269 indoc! {"
6270 «1. This is a numbered list item that is
6271 very long and needs to be wrapped
6272 properly.
6273 2. This is a numbered list item that is
6274 very long and needs to be wrapped
6275 properly.
6276 - This is an unordered list item that is
6277 also very long and should not merge with
6278 the numbered item.ˇ»
6279 "},
6280 indoc! {"
6281 «1. This is a numbered list item that is
6282 very long and needs to be wrapped
6283 properly.
6284 2. This is a numbered list item that is
6285 very long and needs to be wrapped
6286 properly.
6287 - This is an unordered list item that is
6288 also very long and should not merge
6289 with the numbered item.ˇ»
6290 "},
6291 markdown_language.clone(),
6292 &mut cx,
6293 );
6294
6295 // Test that rewrapping maintain indents even when they already exists.
6296 assert_rewrap(
6297 indoc! {"
6298 «1. This is a numbered list
6299 item that is very long and needs to be wrapped properly.
6300 2. This is a numbered list
6301 item that is very long and needs to be wrapped properly.
6302 - This is an unordered list item that is also very long and
6303 should not merge with the numbered item.ˇ»
6304 "},
6305 indoc! {"
6306 «1. This is a numbered list item that is
6307 very long and needs to be wrapped
6308 properly.
6309 2. This is a numbered list item that is
6310 very long and needs to be wrapped
6311 properly.
6312 - This is an unordered list item that is
6313 also very long and should not merge
6314 with the numbered item.ˇ»
6315 "},
6316 markdown_language,
6317 &mut cx,
6318 );
6319
6320 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6321 assert_rewrap(
6322 indoc! {"
6323 ˇThis is a very long line of plain text that will be wrapped.
6324 "},
6325 indoc! {"
6326 ˇThis is a very long line of plain text
6327 that will be wrapped.
6328 "},
6329 plaintext_language.clone(),
6330 &mut cx,
6331 );
6332
6333 // Test that non-commented code acts as a paragraph boundary within a selection
6334 assert_rewrap(
6335 indoc! {"
6336 «// This is the first long comment block to be wrapped.
6337 fn my_func(a: u32);
6338 // This is the second long comment block to be wrapped.ˇ»
6339 "},
6340 indoc! {"
6341 «// This is the first long comment block
6342 // to be wrapped.
6343 fn my_func(a: u32);
6344 // This is the second long comment block
6345 // to be wrapped.ˇ»
6346 "},
6347 rust_language,
6348 &mut cx,
6349 );
6350
6351 // Test rewrapping multiple selections, including ones with blank lines or tabs
6352 assert_rewrap(
6353 indoc! {"
6354 «ˇThis is a very long line that will be wrapped.
6355
6356 This is another paragraph in the same selection.»
6357
6358 «\tThis is a very long indented line that will be wrapped.ˇ»
6359 "},
6360 indoc! {"
6361 «ˇThis is a very long line that will be
6362 wrapped.
6363
6364 This is another paragraph in the same
6365 selection.»
6366
6367 «\tThis is a very long indented line
6368 \tthat will be wrapped.ˇ»
6369 "},
6370 plaintext_language,
6371 &mut cx,
6372 );
6373
6374 // Test that an empty comment line acts as a paragraph boundary
6375 assert_rewrap(
6376 indoc! {"
6377 // ˇThis is a long comment that will be wrapped.
6378 //
6379 // And this is another long comment that will also be wrapped.ˇ
6380 "},
6381 indoc! {"
6382 // ˇThis is a long comment that will be
6383 // wrapped.
6384 //
6385 // And this is another long comment that
6386 // will also be wrapped.ˇ
6387 "},
6388 cpp_language,
6389 &mut cx,
6390 );
6391
6392 #[track_caller]
6393 fn assert_rewrap(
6394 unwrapped_text: &str,
6395 wrapped_text: &str,
6396 language: Arc<Language>,
6397 cx: &mut EditorTestContext,
6398 ) {
6399 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6400 cx.set_state(unwrapped_text);
6401 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6402 cx.assert_editor_state(wrapped_text);
6403 }
6404}
6405
6406#[gpui::test]
6407async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6408 init_test(cx, |settings| {
6409 settings.languages.0.extend([(
6410 "Rust".into(),
6411 LanguageSettingsContent {
6412 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6413 preferred_line_length: Some(40),
6414 ..Default::default()
6415 },
6416 )])
6417 });
6418
6419 let mut cx = EditorTestContext::new(cx).await;
6420
6421 let rust_lang = Arc::new(
6422 Language::new(
6423 LanguageConfig {
6424 name: "Rust".into(),
6425 line_comments: vec!["// ".into()],
6426 block_comment: Some(BlockCommentConfig {
6427 start: "/*".into(),
6428 end: "*/".into(),
6429 prefix: "* ".into(),
6430 tab_size: 1,
6431 }),
6432 documentation_comment: Some(BlockCommentConfig {
6433 start: "/**".into(),
6434 end: "*/".into(),
6435 prefix: "* ".into(),
6436 tab_size: 1,
6437 }),
6438
6439 ..LanguageConfig::default()
6440 },
6441 Some(tree_sitter_rust::LANGUAGE.into()),
6442 )
6443 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6444 .unwrap(),
6445 );
6446
6447 // regular block comment
6448 assert_rewrap(
6449 indoc! {"
6450 /*
6451 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6452 */
6453 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6454 "},
6455 indoc! {"
6456 /*
6457 *ˇ Lorem ipsum dolor sit amet,
6458 * consectetur adipiscing elit.
6459 */
6460 /*
6461 *ˇ Lorem ipsum dolor sit amet,
6462 * consectetur adipiscing elit.
6463 */
6464 "},
6465 rust_lang.clone(),
6466 &mut cx,
6467 );
6468
6469 // indent is respected
6470 assert_rewrap(
6471 indoc! {"
6472 {}
6473 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6474 "},
6475 indoc! {"
6476 {}
6477 /*
6478 *ˇ Lorem ipsum dolor sit amet,
6479 * consectetur adipiscing elit.
6480 */
6481 "},
6482 rust_lang.clone(),
6483 &mut cx,
6484 );
6485
6486 // short block comments with inline delimiters
6487 assert_rewrap(
6488 indoc! {"
6489 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6490 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6491 */
6492 /*
6493 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6494 "},
6495 indoc! {"
6496 /*
6497 *ˇ Lorem ipsum dolor sit amet,
6498 * consectetur adipiscing elit.
6499 */
6500 /*
6501 *ˇ Lorem ipsum dolor sit amet,
6502 * consectetur adipiscing elit.
6503 */
6504 /*
6505 *ˇ Lorem ipsum dolor sit amet,
6506 * consectetur adipiscing elit.
6507 */
6508 "},
6509 rust_lang.clone(),
6510 &mut cx,
6511 );
6512
6513 // multiline block comment with inline start/end delimiters
6514 assert_rewrap(
6515 indoc! {"
6516 /*ˇ Lorem ipsum dolor sit amet,
6517 * consectetur adipiscing elit. */
6518 "},
6519 indoc! {"
6520 /*
6521 *ˇ Lorem ipsum dolor sit amet,
6522 * consectetur adipiscing elit.
6523 */
6524 "},
6525 rust_lang.clone(),
6526 &mut cx,
6527 );
6528
6529 // block comment rewrap still respects paragraph bounds
6530 assert_rewrap(
6531 indoc! {"
6532 /*
6533 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6534 *
6535 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6536 */
6537 "},
6538 indoc! {"
6539 /*
6540 *ˇ Lorem ipsum dolor sit amet,
6541 * consectetur adipiscing elit.
6542 *
6543 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6544 */
6545 "},
6546 rust_lang.clone(),
6547 &mut cx,
6548 );
6549
6550 // documentation comments
6551 assert_rewrap(
6552 indoc! {"
6553 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6554 /**
6555 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6556 */
6557 "},
6558 indoc! {"
6559 /**
6560 *ˇ Lorem ipsum dolor sit amet,
6561 * consectetur adipiscing elit.
6562 */
6563 /**
6564 *ˇ Lorem ipsum dolor sit amet,
6565 * consectetur adipiscing elit.
6566 */
6567 "},
6568 rust_lang.clone(),
6569 &mut cx,
6570 );
6571
6572 // different, adjacent comments
6573 assert_rewrap(
6574 indoc! {"
6575 /**
6576 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6577 */
6578 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6579 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6580 "},
6581 indoc! {"
6582 /**
6583 *ˇ Lorem ipsum dolor sit amet,
6584 * consectetur adipiscing elit.
6585 */
6586 /*
6587 *ˇ Lorem ipsum dolor sit amet,
6588 * consectetur adipiscing elit.
6589 */
6590 //ˇ Lorem ipsum dolor sit amet,
6591 // consectetur adipiscing elit.
6592 "},
6593 rust_lang.clone(),
6594 &mut cx,
6595 );
6596
6597 // selection w/ single short block comment
6598 assert_rewrap(
6599 indoc! {"
6600 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6601 "},
6602 indoc! {"
6603 «/*
6604 * Lorem ipsum dolor sit amet,
6605 * consectetur adipiscing elit.
6606 */ˇ»
6607 "},
6608 rust_lang.clone(),
6609 &mut cx,
6610 );
6611
6612 // rewrapping a single comment w/ abutting comments
6613 assert_rewrap(
6614 indoc! {"
6615 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6616 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6617 "},
6618 indoc! {"
6619 /*
6620 * ˇLorem ipsum dolor sit amet,
6621 * consectetur adipiscing elit.
6622 */
6623 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6624 "},
6625 rust_lang.clone(),
6626 &mut cx,
6627 );
6628
6629 // selection w/ non-abutting short block comments
6630 assert_rewrap(
6631 indoc! {"
6632 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6633
6634 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6635 "},
6636 indoc! {"
6637 «/*
6638 * Lorem ipsum dolor sit amet,
6639 * consectetur adipiscing elit.
6640 */
6641
6642 /*
6643 * Lorem ipsum dolor sit amet,
6644 * consectetur adipiscing elit.
6645 */ˇ»
6646 "},
6647 rust_lang.clone(),
6648 &mut cx,
6649 );
6650
6651 // selection of multiline block comments
6652 assert_rewrap(
6653 indoc! {"
6654 «/* Lorem ipsum dolor sit amet,
6655 * consectetur adipiscing elit. */ˇ»
6656 "},
6657 indoc! {"
6658 «/*
6659 * Lorem ipsum dolor sit amet,
6660 * consectetur adipiscing elit.
6661 */ˇ»
6662 "},
6663 rust_lang.clone(),
6664 &mut cx,
6665 );
6666
6667 // partial selection of multiline block comments
6668 assert_rewrap(
6669 indoc! {"
6670 «/* Lorem ipsum dolor sit amet,ˇ»
6671 * consectetur adipiscing elit. */
6672 /* Lorem ipsum dolor sit amet,
6673 «* consectetur adipiscing elit. */ˇ»
6674 "},
6675 indoc! {"
6676 «/*
6677 * Lorem ipsum dolor sit amet,ˇ»
6678 * consectetur adipiscing elit. */
6679 /* Lorem ipsum dolor sit amet,
6680 «* consectetur adipiscing elit.
6681 */ˇ»
6682 "},
6683 rust_lang.clone(),
6684 &mut cx,
6685 );
6686
6687 // selection w/ abutting short block comments
6688 // TODO: should not be combined; should rewrap as 2 comments
6689 assert_rewrap(
6690 indoc! {"
6691 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6692 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6693 "},
6694 // desired behavior:
6695 // indoc! {"
6696 // «/*
6697 // * Lorem ipsum dolor sit amet,
6698 // * consectetur adipiscing elit.
6699 // */
6700 // /*
6701 // * Lorem ipsum dolor sit amet,
6702 // * consectetur adipiscing elit.
6703 // */ˇ»
6704 // "},
6705 // actual behaviour:
6706 indoc! {"
6707 «/*
6708 * Lorem ipsum dolor sit amet,
6709 * consectetur adipiscing elit. Lorem
6710 * ipsum dolor sit amet, consectetur
6711 * adipiscing elit.
6712 */ˇ»
6713 "},
6714 rust_lang.clone(),
6715 &mut cx,
6716 );
6717
6718 // TODO: same as above, but with delimiters on separate line
6719 // assert_rewrap(
6720 // indoc! {"
6721 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6722 // */
6723 // /*
6724 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6725 // "},
6726 // // desired:
6727 // // indoc! {"
6728 // // «/*
6729 // // * Lorem ipsum dolor sit amet,
6730 // // * consectetur adipiscing elit.
6731 // // */
6732 // // /*
6733 // // * Lorem ipsum dolor sit amet,
6734 // // * consectetur adipiscing elit.
6735 // // */ˇ»
6736 // // "},
6737 // // actual: (but with trailing w/s on the empty lines)
6738 // indoc! {"
6739 // «/*
6740 // * Lorem ipsum dolor sit amet,
6741 // * consectetur adipiscing elit.
6742 // *
6743 // */
6744 // /*
6745 // *
6746 // * Lorem ipsum dolor sit amet,
6747 // * consectetur adipiscing elit.
6748 // */ˇ»
6749 // "},
6750 // rust_lang.clone(),
6751 // &mut cx,
6752 // );
6753
6754 // TODO these are unhandled edge cases; not correct, just documenting known issues
6755 assert_rewrap(
6756 indoc! {"
6757 /*
6758 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6759 */
6760 /*
6761 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6762 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6763 "},
6764 // desired:
6765 // indoc! {"
6766 // /*
6767 // *ˇ Lorem ipsum dolor sit amet,
6768 // * consectetur adipiscing elit.
6769 // */
6770 // /*
6771 // *ˇ Lorem ipsum dolor sit amet,
6772 // * consectetur adipiscing elit.
6773 // */
6774 // /*
6775 // *ˇ Lorem ipsum dolor sit amet
6776 // */ /* consectetur adipiscing elit. */
6777 // "},
6778 // actual:
6779 indoc! {"
6780 /*
6781 //ˇ Lorem ipsum dolor sit amet,
6782 // consectetur adipiscing elit.
6783 */
6784 /*
6785 * //ˇ Lorem ipsum dolor sit amet,
6786 * consectetur adipiscing elit.
6787 */
6788 /*
6789 *ˇ Lorem ipsum dolor sit amet */ /*
6790 * consectetur adipiscing elit.
6791 */
6792 "},
6793 rust_lang,
6794 &mut cx,
6795 );
6796
6797 #[track_caller]
6798 fn assert_rewrap(
6799 unwrapped_text: &str,
6800 wrapped_text: &str,
6801 language: Arc<Language>,
6802 cx: &mut EditorTestContext,
6803 ) {
6804 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6805 cx.set_state(unwrapped_text);
6806 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6807 cx.assert_editor_state(wrapped_text);
6808 }
6809}
6810
6811#[gpui::test]
6812async fn test_hard_wrap(cx: &mut TestAppContext) {
6813 init_test(cx, |_| {});
6814 let mut cx = EditorTestContext::new(cx).await;
6815
6816 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6817 cx.update_editor(|editor, _, cx| {
6818 editor.set_hard_wrap(Some(14), cx);
6819 });
6820
6821 cx.set_state(indoc!(
6822 "
6823 one two three ˇ
6824 "
6825 ));
6826 cx.simulate_input("four");
6827 cx.run_until_parked();
6828
6829 cx.assert_editor_state(indoc!(
6830 "
6831 one two three
6832 fourˇ
6833 "
6834 ));
6835
6836 cx.update_editor(|editor, window, cx| {
6837 editor.newline(&Default::default(), window, cx);
6838 });
6839 cx.run_until_parked();
6840 cx.assert_editor_state(indoc!(
6841 "
6842 one two three
6843 four
6844 ˇ
6845 "
6846 ));
6847
6848 cx.simulate_input("five");
6849 cx.run_until_parked();
6850 cx.assert_editor_state(indoc!(
6851 "
6852 one two three
6853 four
6854 fiveˇ
6855 "
6856 ));
6857
6858 cx.update_editor(|editor, window, cx| {
6859 editor.newline(&Default::default(), window, cx);
6860 });
6861 cx.run_until_parked();
6862 cx.simulate_input("# ");
6863 cx.run_until_parked();
6864 cx.assert_editor_state(indoc!(
6865 "
6866 one two three
6867 four
6868 five
6869 # ˇ
6870 "
6871 ));
6872
6873 cx.update_editor(|editor, window, cx| {
6874 editor.newline(&Default::default(), window, cx);
6875 });
6876 cx.run_until_parked();
6877 cx.assert_editor_state(indoc!(
6878 "
6879 one two three
6880 four
6881 five
6882 #\x20
6883 #ˇ
6884 "
6885 ));
6886
6887 cx.simulate_input(" 6");
6888 cx.run_until_parked();
6889 cx.assert_editor_state(indoc!(
6890 "
6891 one two three
6892 four
6893 five
6894 #
6895 # 6ˇ
6896 "
6897 ));
6898}
6899
6900#[gpui::test]
6901async fn test_cut_line_ends(cx: &mut TestAppContext) {
6902 init_test(cx, |_| {});
6903
6904 let mut cx = EditorTestContext::new(cx).await;
6905
6906 cx.set_state(indoc! {"The quick brownˇ"});
6907 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6908 cx.assert_editor_state(indoc! {"The quick brownˇ"});
6909
6910 cx.set_state(indoc! {"The emacs foxˇ"});
6911 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6912 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
6913
6914 cx.set_state(indoc! {"
6915 The quick« brownˇ»
6916 fox jumps overˇ
6917 the lazy dog"});
6918 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6919 cx.assert_editor_state(indoc! {"
6920 The quickˇ
6921 ˇthe lazy dog"});
6922
6923 cx.set_state(indoc! {"
6924 The quick« brownˇ»
6925 fox jumps overˇ
6926 the lazy dog"});
6927 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6928 cx.assert_editor_state(indoc! {"
6929 The quickˇ
6930 fox jumps overˇthe lazy dog"});
6931
6932 cx.set_state(indoc! {"
6933 The quick« brownˇ»
6934 fox jumps overˇ
6935 the lazy dog"});
6936 cx.update_editor(|e, window, cx| {
6937 e.cut_to_end_of_line(
6938 &CutToEndOfLine {
6939 stop_at_newlines: true,
6940 },
6941 window,
6942 cx,
6943 )
6944 });
6945 cx.assert_editor_state(indoc! {"
6946 The quickˇ
6947 fox jumps overˇ
6948 the lazy dog"});
6949
6950 cx.set_state(indoc! {"
6951 The quick« brownˇ»
6952 fox jumps overˇ
6953 the lazy dog"});
6954 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6955 cx.assert_editor_state(indoc! {"
6956 The quickˇ
6957 fox jumps overˇthe lazy dog"});
6958}
6959
6960#[gpui::test]
6961async fn test_clipboard(cx: &mut TestAppContext) {
6962 init_test(cx, |_| {});
6963
6964 let mut cx = EditorTestContext::new(cx).await;
6965
6966 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
6967 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6968 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
6969
6970 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
6971 cx.set_state("two ˇfour ˇsix ˇ");
6972 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6973 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
6974
6975 // Paste again but with only two cursors. Since the number of cursors doesn't
6976 // match the number of slices in the clipboard, the entire clipboard text
6977 // is pasted at each cursor.
6978 cx.set_state("ˇtwo one✅ four three six five ˇ");
6979 cx.update_editor(|e, window, cx| {
6980 e.handle_input("( ", window, cx);
6981 e.paste(&Paste, window, cx);
6982 e.handle_input(") ", window, cx);
6983 });
6984 cx.assert_editor_state(
6985 &([
6986 "( one✅ ",
6987 "three ",
6988 "five ) ˇtwo one✅ four three six five ( one✅ ",
6989 "three ",
6990 "five ) ˇ",
6991 ]
6992 .join("\n")),
6993 );
6994
6995 // Cut with three selections, one of which is full-line.
6996 cx.set_state(indoc! {"
6997 1«2ˇ»3
6998 4ˇ567
6999 «8ˇ»9"});
7000 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7001 cx.assert_editor_state(indoc! {"
7002 1ˇ3
7003 ˇ9"});
7004
7005 // Paste with three selections, noticing how the copied selection that was full-line
7006 // gets inserted before the second cursor.
7007 cx.set_state(indoc! {"
7008 1ˇ3
7009 9ˇ
7010 «oˇ»ne"});
7011 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7012 cx.assert_editor_state(indoc! {"
7013 12ˇ3
7014 4567
7015 9ˇ
7016 8ˇne"});
7017
7018 // Copy with a single cursor only, which writes the whole line into the clipboard.
7019 cx.set_state(indoc! {"
7020 The quick brown
7021 fox juˇmps over
7022 the lazy dog"});
7023 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7024 assert_eq!(
7025 cx.read_from_clipboard()
7026 .and_then(|item| item.text().as_deref().map(str::to_string)),
7027 Some("fox jumps over\n".to_string())
7028 );
7029
7030 // Paste with three selections, noticing how the copied full-line selection is inserted
7031 // before the empty selections but replaces the selection that is non-empty.
7032 cx.set_state(indoc! {"
7033 Tˇhe quick brown
7034 «foˇ»x jumps over
7035 tˇhe lazy dog"});
7036 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7037 cx.assert_editor_state(indoc! {"
7038 fox jumps over
7039 Tˇhe quick brown
7040 fox jumps over
7041 ˇx jumps over
7042 fox jumps over
7043 tˇhe lazy dog"});
7044}
7045
7046#[gpui::test]
7047async fn test_copy_trim(cx: &mut TestAppContext) {
7048 init_test(cx, |_| {});
7049
7050 let mut cx = EditorTestContext::new(cx).await;
7051 cx.set_state(
7052 r#" «for selection in selections.iter() {
7053 let mut start = selection.start;
7054 let mut end = selection.end;
7055 let is_entire_line = selection.is_empty();
7056 if is_entire_line {
7057 start = Point::new(start.row, 0);ˇ»
7058 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7059 }
7060 "#,
7061 );
7062 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7063 assert_eq!(
7064 cx.read_from_clipboard()
7065 .and_then(|item| item.text().as_deref().map(str::to_string)),
7066 Some(
7067 "for selection in selections.iter() {
7068 let mut start = selection.start;
7069 let mut end = selection.end;
7070 let is_entire_line = selection.is_empty();
7071 if is_entire_line {
7072 start = Point::new(start.row, 0);"
7073 .to_string()
7074 ),
7075 "Regular copying preserves all indentation selected",
7076 );
7077 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7078 assert_eq!(
7079 cx.read_from_clipboard()
7080 .and_then(|item| item.text().as_deref().map(str::to_string)),
7081 Some(
7082 "for selection in selections.iter() {
7083let mut start = selection.start;
7084let mut end = selection.end;
7085let is_entire_line = selection.is_empty();
7086if is_entire_line {
7087 start = Point::new(start.row, 0);"
7088 .to_string()
7089 ),
7090 "Copying with stripping should strip all leading whitespaces"
7091 );
7092
7093 cx.set_state(
7094 r#" « for selection in selections.iter() {
7095 let mut start = selection.start;
7096 let mut end = selection.end;
7097 let is_entire_line = selection.is_empty();
7098 if is_entire_line {
7099 start = Point::new(start.row, 0);ˇ»
7100 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7101 }
7102 "#,
7103 );
7104 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7105 assert_eq!(
7106 cx.read_from_clipboard()
7107 .and_then(|item| item.text().as_deref().map(str::to_string)),
7108 Some(
7109 " for selection in selections.iter() {
7110 let mut start = selection.start;
7111 let mut end = selection.end;
7112 let is_entire_line = selection.is_empty();
7113 if is_entire_line {
7114 start = Point::new(start.row, 0);"
7115 .to_string()
7116 ),
7117 "Regular copying preserves all indentation selected",
7118 );
7119 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7120 assert_eq!(
7121 cx.read_from_clipboard()
7122 .and_then(|item| item.text().as_deref().map(str::to_string)),
7123 Some(
7124 "for selection in selections.iter() {
7125let mut start = selection.start;
7126let mut end = selection.end;
7127let is_entire_line = selection.is_empty();
7128if is_entire_line {
7129 start = Point::new(start.row, 0);"
7130 .to_string()
7131 ),
7132 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7133 );
7134
7135 cx.set_state(
7136 r#" «ˇ for selection in selections.iter() {
7137 let mut start = selection.start;
7138 let mut end = selection.end;
7139 let is_entire_line = selection.is_empty();
7140 if is_entire_line {
7141 start = Point::new(start.row, 0);»
7142 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7143 }
7144 "#,
7145 );
7146 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7147 assert_eq!(
7148 cx.read_from_clipboard()
7149 .and_then(|item| item.text().as_deref().map(str::to_string)),
7150 Some(
7151 " for selection in selections.iter() {
7152 let mut start = selection.start;
7153 let mut end = selection.end;
7154 let is_entire_line = selection.is_empty();
7155 if is_entire_line {
7156 start = Point::new(start.row, 0);"
7157 .to_string()
7158 ),
7159 "Regular copying for reverse selection works the same",
7160 );
7161 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7162 assert_eq!(
7163 cx.read_from_clipboard()
7164 .and_then(|item| item.text().as_deref().map(str::to_string)),
7165 Some(
7166 "for selection in selections.iter() {
7167let mut start = selection.start;
7168let mut end = selection.end;
7169let is_entire_line = selection.is_empty();
7170if is_entire_line {
7171 start = Point::new(start.row, 0);"
7172 .to_string()
7173 ),
7174 "Copying with stripping for reverse selection works the same"
7175 );
7176
7177 cx.set_state(
7178 r#" for selection «in selections.iter() {
7179 let mut start = selection.start;
7180 let mut end = selection.end;
7181 let is_entire_line = selection.is_empty();
7182 if is_entire_line {
7183 start = Point::new(start.row, 0);ˇ»
7184 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7185 }
7186 "#,
7187 );
7188 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7189 assert_eq!(
7190 cx.read_from_clipboard()
7191 .and_then(|item| item.text().as_deref().map(str::to_string)),
7192 Some(
7193 "in selections.iter() {
7194 let mut start = selection.start;
7195 let mut end = selection.end;
7196 let is_entire_line = selection.is_empty();
7197 if is_entire_line {
7198 start = Point::new(start.row, 0);"
7199 .to_string()
7200 ),
7201 "When selecting past the indent, the copying works as usual",
7202 );
7203 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7204 assert_eq!(
7205 cx.read_from_clipboard()
7206 .and_then(|item| item.text().as_deref().map(str::to_string)),
7207 Some(
7208 "in selections.iter() {
7209 let mut start = selection.start;
7210 let mut end = selection.end;
7211 let is_entire_line = selection.is_empty();
7212 if is_entire_line {
7213 start = Point::new(start.row, 0);"
7214 .to_string()
7215 ),
7216 "When selecting past the indent, nothing is trimmed"
7217 );
7218
7219 cx.set_state(
7220 r#" «for selection in selections.iter() {
7221 let mut start = selection.start;
7222
7223 let mut end = selection.end;
7224 let is_entire_line = selection.is_empty();
7225 if is_entire_line {
7226 start = Point::new(start.row, 0);
7227ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7228 }
7229 "#,
7230 );
7231 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7232 assert_eq!(
7233 cx.read_from_clipboard()
7234 .and_then(|item| item.text().as_deref().map(str::to_string)),
7235 Some(
7236 "for selection in selections.iter() {
7237let mut start = selection.start;
7238
7239let mut end = selection.end;
7240let is_entire_line = selection.is_empty();
7241if is_entire_line {
7242 start = Point::new(start.row, 0);
7243"
7244 .to_string()
7245 ),
7246 "Copying with stripping should ignore empty lines"
7247 );
7248}
7249
7250#[gpui::test]
7251async fn test_paste_multiline(cx: &mut TestAppContext) {
7252 init_test(cx, |_| {});
7253
7254 let mut cx = EditorTestContext::new(cx).await;
7255 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7256
7257 // Cut an indented block, without the leading whitespace.
7258 cx.set_state(indoc! {"
7259 const a: B = (
7260 c(),
7261 «d(
7262 e,
7263 f
7264 )ˇ»
7265 );
7266 "});
7267 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7268 cx.assert_editor_state(indoc! {"
7269 const a: B = (
7270 c(),
7271 ˇ
7272 );
7273 "});
7274
7275 // Paste it at the same position.
7276 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7277 cx.assert_editor_state(indoc! {"
7278 const a: B = (
7279 c(),
7280 d(
7281 e,
7282 f
7283 )ˇ
7284 );
7285 "});
7286
7287 // Paste it at a line with a lower indent level.
7288 cx.set_state(indoc! {"
7289 ˇ
7290 const a: B = (
7291 c(),
7292 );
7293 "});
7294 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7295 cx.assert_editor_state(indoc! {"
7296 d(
7297 e,
7298 f
7299 )ˇ
7300 const a: B = (
7301 c(),
7302 );
7303 "});
7304
7305 // Cut an indented block, with the leading whitespace.
7306 cx.set_state(indoc! {"
7307 const a: B = (
7308 c(),
7309 « d(
7310 e,
7311 f
7312 )
7313 ˇ»);
7314 "});
7315 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7316 cx.assert_editor_state(indoc! {"
7317 const a: B = (
7318 c(),
7319 ˇ);
7320 "});
7321
7322 // Paste it at the same position.
7323 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7324 cx.assert_editor_state(indoc! {"
7325 const a: B = (
7326 c(),
7327 d(
7328 e,
7329 f
7330 )
7331 ˇ);
7332 "});
7333
7334 // Paste it at a line with a higher indent level.
7335 cx.set_state(indoc! {"
7336 const a: B = (
7337 c(),
7338 d(
7339 e,
7340 fˇ
7341 )
7342 );
7343 "});
7344 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7345 cx.assert_editor_state(indoc! {"
7346 const a: B = (
7347 c(),
7348 d(
7349 e,
7350 f d(
7351 e,
7352 f
7353 )
7354 ˇ
7355 )
7356 );
7357 "});
7358
7359 // Copy an indented block, starting mid-line
7360 cx.set_state(indoc! {"
7361 const a: B = (
7362 c(),
7363 somethin«g(
7364 e,
7365 f
7366 )ˇ»
7367 );
7368 "});
7369 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7370
7371 // Paste it on a line with a lower indent level
7372 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7373 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7374 cx.assert_editor_state(indoc! {"
7375 const a: B = (
7376 c(),
7377 something(
7378 e,
7379 f
7380 )
7381 );
7382 g(
7383 e,
7384 f
7385 )ˇ"});
7386}
7387
7388#[gpui::test]
7389async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7390 init_test(cx, |_| {});
7391
7392 cx.write_to_clipboard(ClipboardItem::new_string(
7393 " d(\n e\n );\n".into(),
7394 ));
7395
7396 let mut cx = EditorTestContext::new(cx).await;
7397 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7398
7399 cx.set_state(indoc! {"
7400 fn a() {
7401 b();
7402 if c() {
7403 ˇ
7404 }
7405 }
7406 "});
7407
7408 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7409 cx.assert_editor_state(indoc! {"
7410 fn a() {
7411 b();
7412 if c() {
7413 d(
7414 e
7415 );
7416 ˇ
7417 }
7418 }
7419 "});
7420
7421 cx.set_state(indoc! {"
7422 fn a() {
7423 b();
7424 ˇ
7425 }
7426 "});
7427
7428 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7429 cx.assert_editor_state(indoc! {"
7430 fn a() {
7431 b();
7432 d(
7433 e
7434 );
7435 ˇ
7436 }
7437 "});
7438}
7439
7440#[gpui::test]
7441fn test_select_all(cx: &mut TestAppContext) {
7442 init_test(cx, |_| {});
7443
7444 let editor = cx.add_window(|window, cx| {
7445 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7446 build_editor(buffer, window, cx)
7447 });
7448 _ = editor.update(cx, |editor, window, cx| {
7449 editor.select_all(&SelectAll, window, cx);
7450 assert_eq!(
7451 editor.selections.display_ranges(cx),
7452 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7453 );
7454 });
7455}
7456
7457#[gpui::test]
7458fn test_select_line(cx: &mut TestAppContext) {
7459 init_test(cx, |_| {});
7460
7461 let editor = cx.add_window(|window, cx| {
7462 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7463 build_editor(buffer, window, cx)
7464 });
7465 _ = editor.update(cx, |editor, window, cx| {
7466 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7467 s.select_display_ranges([
7468 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7469 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7470 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7471 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7472 ])
7473 });
7474 editor.select_line(&SelectLine, window, cx);
7475 assert_eq!(
7476 editor.selections.display_ranges(cx),
7477 vec![
7478 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7479 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7480 ]
7481 );
7482 });
7483
7484 _ = editor.update(cx, |editor, window, cx| {
7485 editor.select_line(&SelectLine, window, cx);
7486 assert_eq!(
7487 editor.selections.display_ranges(cx),
7488 vec![
7489 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7490 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7491 ]
7492 );
7493 });
7494
7495 _ = editor.update(cx, |editor, window, cx| {
7496 editor.select_line(&SelectLine, window, cx);
7497 assert_eq!(
7498 editor.selections.display_ranges(cx),
7499 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7500 );
7501 });
7502}
7503
7504#[gpui::test]
7505async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7506 init_test(cx, |_| {});
7507 let mut cx = EditorTestContext::new(cx).await;
7508
7509 #[track_caller]
7510 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7511 cx.set_state(initial_state);
7512 cx.update_editor(|e, window, cx| {
7513 e.split_selection_into_lines(&Default::default(), window, cx)
7514 });
7515 cx.assert_editor_state(expected_state);
7516 }
7517
7518 // Selection starts and ends at the middle of lines, left-to-right
7519 test(
7520 &mut cx,
7521 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7522 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7523 );
7524 // Same thing, right-to-left
7525 test(
7526 &mut cx,
7527 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7528 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7529 );
7530
7531 // Whole buffer, left-to-right, last line *doesn't* end with newline
7532 test(
7533 &mut cx,
7534 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7535 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7536 );
7537 // Same thing, right-to-left
7538 test(
7539 &mut cx,
7540 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7541 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7542 );
7543
7544 // Whole buffer, left-to-right, last line ends with newline
7545 test(
7546 &mut cx,
7547 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7548 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7549 );
7550 // Same thing, right-to-left
7551 test(
7552 &mut cx,
7553 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7554 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7555 );
7556
7557 // Starts at the end of a line, ends at the start of another
7558 test(
7559 &mut cx,
7560 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7561 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7562 );
7563}
7564
7565#[gpui::test]
7566async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7567 init_test(cx, |_| {});
7568
7569 let editor = cx.add_window(|window, cx| {
7570 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7571 build_editor(buffer, window, cx)
7572 });
7573
7574 // setup
7575 _ = editor.update(cx, |editor, window, cx| {
7576 editor.fold_creases(
7577 vec![
7578 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7579 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7580 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7581 ],
7582 true,
7583 window,
7584 cx,
7585 );
7586 assert_eq!(
7587 editor.display_text(cx),
7588 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7589 );
7590 });
7591
7592 _ = editor.update(cx, |editor, window, cx| {
7593 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7594 s.select_display_ranges([
7595 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7596 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7597 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7598 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7599 ])
7600 });
7601 editor.split_selection_into_lines(&Default::default(), window, cx);
7602 assert_eq!(
7603 editor.display_text(cx),
7604 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7605 );
7606 });
7607 EditorTestContext::for_editor(editor, cx)
7608 .await
7609 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7610
7611 _ = editor.update(cx, |editor, window, cx| {
7612 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7613 s.select_display_ranges([
7614 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7615 ])
7616 });
7617 editor.split_selection_into_lines(&Default::default(), window, cx);
7618 assert_eq!(
7619 editor.display_text(cx),
7620 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7621 );
7622 assert_eq!(
7623 editor.selections.display_ranges(cx),
7624 [
7625 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7626 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7627 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7628 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7629 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7630 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7631 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7632 ]
7633 );
7634 });
7635 EditorTestContext::for_editor(editor, cx)
7636 .await
7637 .assert_editor_state(
7638 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7639 );
7640}
7641
7642#[gpui::test]
7643async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7644 init_test(cx, |_| {});
7645
7646 let mut cx = EditorTestContext::new(cx).await;
7647
7648 cx.set_state(indoc!(
7649 r#"abc
7650 defˇghi
7651
7652 jk
7653 nlmo
7654 "#
7655 ));
7656
7657 cx.update_editor(|editor, window, cx| {
7658 editor.add_selection_above(&Default::default(), window, cx);
7659 });
7660
7661 cx.assert_editor_state(indoc!(
7662 r#"abcˇ
7663 defˇghi
7664
7665 jk
7666 nlmo
7667 "#
7668 ));
7669
7670 cx.update_editor(|editor, window, cx| {
7671 editor.add_selection_above(&Default::default(), window, cx);
7672 });
7673
7674 cx.assert_editor_state(indoc!(
7675 r#"abcˇ
7676 defˇghi
7677
7678 jk
7679 nlmo
7680 "#
7681 ));
7682
7683 cx.update_editor(|editor, window, cx| {
7684 editor.add_selection_below(&Default::default(), window, cx);
7685 });
7686
7687 cx.assert_editor_state(indoc!(
7688 r#"abc
7689 defˇghi
7690
7691 jk
7692 nlmo
7693 "#
7694 ));
7695
7696 cx.update_editor(|editor, window, cx| {
7697 editor.undo_selection(&Default::default(), window, cx);
7698 });
7699
7700 cx.assert_editor_state(indoc!(
7701 r#"abcˇ
7702 defˇghi
7703
7704 jk
7705 nlmo
7706 "#
7707 ));
7708
7709 cx.update_editor(|editor, window, cx| {
7710 editor.redo_selection(&Default::default(), window, cx);
7711 });
7712
7713 cx.assert_editor_state(indoc!(
7714 r#"abc
7715 defˇghi
7716
7717 jk
7718 nlmo
7719 "#
7720 ));
7721
7722 cx.update_editor(|editor, window, cx| {
7723 editor.add_selection_below(&Default::default(), window, cx);
7724 });
7725
7726 cx.assert_editor_state(indoc!(
7727 r#"abc
7728 defˇghi
7729 ˇ
7730 jk
7731 nlmo
7732 "#
7733 ));
7734
7735 cx.update_editor(|editor, window, cx| {
7736 editor.add_selection_below(&Default::default(), window, cx);
7737 });
7738
7739 cx.assert_editor_state(indoc!(
7740 r#"abc
7741 defˇghi
7742 ˇ
7743 jkˇ
7744 nlmo
7745 "#
7746 ));
7747
7748 cx.update_editor(|editor, window, cx| {
7749 editor.add_selection_below(&Default::default(), window, cx);
7750 });
7751
7752 cx.assert_editor_state(indoc!(
7753 r#"abc
7754 defˇghi
7755 ˇ
7756 jkˇ
7757 nlmˇo
7758 "#
7759 ));
7760
7761 cx.update_editor(|editor, window, cx| {
7762 editor.add_selection_below(&Default::default(), window, cx);
7763 });
7764
7765 cx.assert_editor_state(indoc!(
7766 r#"abc
7767 defˇghi
7768 ˇ
7769 jkˇ
7770 nlmˇo
7771 ˇ"#
7772 ));
7773
7774 // change selections
7775 cx.set_state(indoc!(
7776 r#"abc
7777 def«ˇg»hi
7778
7779 jk
7780 nlmo
7781 "#
7782 ));
7783
7784 cx.update_editor(|editor, window, cx| {
7785 editor.add_selection_below(&Default::default(), window, cx);
7786 });
7787
7788 cx.assert_editor_state(indoc!(
7789 r#"abc
7790 def«ˇg»hi
7791
7792 jk
7793 nlm«ˇo»
7794 "#
7795 ));
7796
7797 cx.update_editor(|editor, window, cx| {
7798 editor.add_selection_below(&Default::default(), window, cx);
7799 });
7800
7801 cx.assert_editor_state(indoc!(
7802 r#"abc
7803 def«ˇg»hi
7804
7805 jk
7806 nlm«ˇo»
7807 "#
7808 ));
7809
7810 cx.update_editor(|editor, window, cx| {
7811 editor.add_selection_above(&Default::default(), window, cx);
7812 });
7813
7814 cx.assert_editor_state(indoc!(
7815 r#"abc
7816 def«ˇg»hi
7817
7818 jk
7819 nlmo
7820 "#
7821 ));
7822
7823 cx.update_editor(|editor, window, cx| {
7824 editor.add_selection_above(&Default::default(), window, cx);
7825 });
7826
7827 cx.assert_editor_state(indoc!(
7828 r#"abc
7829 def«ˇg»hi
7830
7831 jk
7832 nlmo
7833 "#
7834 ));
7835
7836 // Change selections again
7837 cx.set_state(indoc!(
7838 r#"a«bc
7839 defgˇ»hi
7840
7841 jk
7842 nlmo
7843 "#
7844 ));
7845
7846 cx.update_editor(|editor, window, cx| {
7847 editor.add_selection_below(&Default::default(), window, cx);
7848 });
7849
7850 cx.assert_editor_state(indoc!(
7851 r#"a«bcˇ»
7852 d«efgˇ»hi
7853
7854 j«kˇ»
7855 nlmo
7856 "#
7857 ));
7858
7859 cx.update_editor(|editor, window, cx| {
7860 editor.add_selection_below(&Default::default(), window, cx);
7861 });
7862 cx.assert_editor_state(indoc!(
7863 r#"a«bcˇ»
7864 d«efgˇ»hi
7865
7866 j«kˇ»
7867 n«lmoˇ»
7868 "#
7869 ));
7870 cx.update_editor(|editor, window, cx| {
7871 editor.add_selection_above(&Default::default(), window, cx);
7872 });
7873
7874 cx.assert_editor_state(indoc!(
7875 r#"a«bcˇ»
7876 d«efgˇ»hi
7877
7878 j«kˇ»
7879 nlmo
7880 "#
7881 ));
7882
7883 // Change selections again
7884 cx.set_state(indoc!(
7885 r#"abc
7886 d«ˇefghi
7887
7888 jk
7889 nlm»o
7890 "#
7891 ));
7892
7893 cx.update_editor(|editor, window, cx| {
7894 editor.add_selection_above(&Default::default(), window, cx);
7895 });
7896
7897 cx.assert_editor_state(indoc!(
7898 r#"a«ˇbc»
7899 d«ˇef»ghi
7900
7901 j«ˇk»
7902 n«ˇlm»o
7903 "#
7904 ));
7905
7906 cx.update_editor(|editor, window, cx| {
7907 editor.add_selection_below(&Default::default(), window, cx);
7908 });
7909
7910 cx.assert_editor_state(indoc!(
7911 r#"abc
7912 d«ˇef»ghi
7913
7914 j«ˇk»
7915 n«ˇlm»o
7916 "#
7917 ));
7918}
7919
7920#[gpui::test]
7921async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
7922 init_test(cx, |_| {});
7923 let mut cx = EditorTestContext::new(cx).await;
7924
7925 cx.set_state(indoc!(
7926 r#"line onˇe
7927 liˇne two
7928 line three
7929 line four"#
7930 ));
7931
7932 cx.update_editor(|editor, window, cx| {
7933 editor.add_selection_below(&Default::default(), window, cx);
7934 });
7935
7936 // test multiple cursors expand in the same direction
7937 cx.assert_editor_state(indoc!(
7938 r#"line onˇe
7939 liˇne twˇo
7940 liˇne three
7941 line four"#
7942 ));
7943
7944 cx.update_editor(|editor, window, cx| {
7945 editor.add_selection_below(&Default::default(), window, cx);
7946 });
7947
7948 cx.update_editor(|editor, window, cx| {
7949 editor.add_selection_below(&Default::default(), window, cx);
7950 });
7951
7952 // test multiple cursors expand below overflow
7953 cx.assert_editor_state(indoc!(
7954 r#"line onˇe
7955 liˇne twˇo
7956 liˇne thˇree
7957 liˇne foˇur"#
7958 ));
7959
7960 cx.update_editor(|editor, window, cx| {
7961 editor.add_selection_above(&Default::default(), window, cx);
7962 });
7963
7964 // test multiple cursors retrieves back correctly
7965 cx.assert_editor_state(indoc!(
7966 r#"line onˇe
7967 liˇne twˇo
7968 liˇne thˇree
7969 line four"#
7970 ));
7971
7972 cx.update_editor(|editor, window, cx| {
7973 editor.add_selection_above(&Default::default(), window, cx);
7974 });
7975
7976 cx.update_editor(|editor, window, cx| {
7977 editor.add_selection_above(&Default::default(), window, cx);
7978 });
7979
7980 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
7981 cx.assert_editor_state(indoc!(
7982 r#"liˇne onˇe
7983 liˇne two
7984 line three
7985 line four"#
7986 ));
7987
7988 cx.update_editor(|editor, window, cx| {
7989 editor.undo_selection(&Default::default(), window, cx);
7990 });
7991
7992 // test undo
7993 cx.assert_editor_state(indoc!(
7994 r#"line onˇe
7995 liˇne twˇo
7996 line three
7997 line four"#
7998 ));
7999
8000 cx.update_editor(|editor, window, cx| {
8001 editor.redo_selection(&Default::default(), window, cx);
8002 });
8003
8004 // test redo
8005 cx.assert_editor_state(indoc!(
8006 r#"liˇne onˇe
8007 liˇne two
8008 line three
8009 line four"#
8010 ));
8011
8012 cx.set_state(indoc!(
8013 r#"abcd
8014 ef«ghˇ»
8015 ijkl
8016 «mˇ»nop"#
8017 ));
8018
8019 cx.update_editor(|editor, window, cx| {
8020 editor.add_selection_above(&Default::default(), window, cx);
8021 });
8022
8023 // test multiple selections expand in the same direction
8024 cx.assert_editor_state(indoc!(
8025 r#"ab«cdˇ»
8026 ef«ghˇ»
8027 «iˇ»jkl
8028 «mˇ»nop"#
8029 ));
8030
8031 cx.update_editor(|editor, window, cx| {
8032 editor.add_selection_above(&Default::default(), window, cx);
8033 });
8034
8035 // test multiple selection upward overflow
8036 cx.assert_editor_state(indoc!(
8037 r#"ab«cdˇ»
8038 «eˇ»f«ghˇ»
8039 «iˇ»jkl
8040 «mˇ»nop"#
8041 ));
8042
8043 cx.update_editor(|editor, window, cx| {
8044 editor.add_selection_below(&Default::default(), window, cx);
8045 });
8046
8047 // test multiple selection retrieves back correctly
8048 cx.assert_editor_state(indoc!(
8049 r#"abcd
8050 ef«ghˇ»
8051 «iˇ»jkl
8052 «mˇ»nop"#
8053 ));
8054
8055 cx.update_editor(|editor, window, cx| {
8056 editor.add_selection_below(&Default::default(), window, cx);
8057 });
8058
8059 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8060 cx.assert_editor_state(indoc!(
8061 r#"abcd
8062 ef«ghˇ»
8063 ij«klˇ»
8064 «mˇ»nop"#
8065 ));
8066
8067 cx.update_editor(|editor, window, cx| {
8068 editor.undo_selection(&Default::default(), window, cx);
8069 });
8070
8071 // test undo
8072 cx.assert_editor_state(indoc!(
8073 r#"abcd
8074 ef«ghˇ»
8075 «iˇ»jkl
8076 «mˇ»nop"#
8077 ));
8078
8079 cx.update_editor(|editor, window, cx| {
8080 editor.redo_selection(&Default::default(), window, cx);
8081 });
8082
8083 // test redo
8084 cx.assert_editor_state(indoc!(
8085 r#"abcd
8086 ef«ghˇ»
8087 ij«klˇ»
8088 «mˇ»nop"#
8089 ));
8090}
8091
8092#[gpui::test]
8093async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8094 init_test(cx, |_| {});
8095 let mut cx = EditorTestContext::new(cx).await;
8096
8097 cx.set_state(indoc!(
8098 r#"line onˇe
8099 liˇne two
8100 line three
8101 line four"#
8102 ));
8103
8104 cx.update_editor(|editor, window, cx| {
8105 editor.add_selection_below(&Default::default(), window, cx);
8106 editor.add_selection_below(&Default::default(), window, cx);
8107 editor.add_selection_below(&Default::default(), window, cx);
8108 });
8109
8110 // initial state with two multi cursor groups
8111 cx.assert_editor_state(indoc!(
8112 r#"line onˇe
8113 liˇne twˇo
8114 liˇne thˇree
8115 liˇne foˇur"#
8116 ));
8117
8118 // add single cursor in middle - simulate opt click
8119 cx.update_editor(|editor, window, cx| {
8120 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8121 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8122 editor.end_selection(window, cx);
8123 });
8124
8125 cx.assert_editor_state(indoc!(
8126 r#"line onˇe
8127 liˇne twˇo
8128 liˇneˇ thˇree
8129 liˇne foˇur"#
8130 ));
8131
8132 cx.update_editor(|editor, window, cx| {
8133 editor.add_selection_above(&Default::default(), window, cx);
8134 });
8135
8136 // test new added selection expands above and existing selection shrinks
8137 cx.assert_editor_state(indoc!(
8138 r#"line onˇe
8139 liˇneˇ twˇo
8140 liˇneˇ thˇree
8141 line four"#
8142 ));
8143
8144 cx.update_editor(|editor, window, cx| {
8145 editor.add_selection_above(&Default::default(), window, cx);
8146 });
8147
8148 // test new added selection expands above and existing selection shrinks
8149 cx.assert_editor_state(indoc!(
8150 r#"lineˇ onˇe
8151 liˇneˇ twˇo
8152 lineˇ three
8153 line four"#
8154 ));
8155
8156 // intial state with two selection groups
8157 cx.set_state(indoc!(
8158 r#"abcd
8159 ef«ghˇ»
8160 ijkl
8161 «mˇ»nop"#
8162 ));
8163
8164 cx.update_editor(|editor, window, cx| {
8165 editor.add_selection_above(&Default::default(), window, cx);
8166 editor.add_selection_above(&Default::default(), window, cx);
8167 });
8168
8169 cx.assert_editor_state(indoc!(
8170 r#"ab«cdˇ»
8171 «eˇ»f«ghˇ»
8172 «iˇ»jkl
8173 «mˇ»nop"#
8174 ));
8175
8176 // add single selection in middle - simulate opt drag
8177 cx.update_editor(|editor, window, cx| {
8178 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8179 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8180 editor.update_selection(
8181 DisplayPoint::new(DisplayRow(2), 4),
8182 0,
8183 gpui::Point::<f32>::default(),
8184 window,
8185 cx,
8186 );
8187 editor.end_selection(window, cx);
8188 });
8189
8190 cx.assert_editor_state(indoc!(
8191 r#"ab«cdˇ»
8192 «eˇ»f«ghˇ»
8193 «iˇ»jk«lˇ»
8194 «mˇ»nop"#
8195 ));
8196
8197 cx.update_editor(|editor, window, cx| {
8198 editor.add_selection_below(&Default::default(), window, cx);
8199 });
8200
8201 // test new added selection expands below, others shrinks from above
8202 cx.assert_editor_state(indoc!(
8203 r#"abcd
8204 ef«ghˇ»
8205 «iˇ»jk«lˇ»
8206 «mˇ»no«pˇ»"#
8207 ));
8208}
8209
8210#[gpui::test]
8211async fn test_select_next(cx: &mut TestAppContext) {
8212 init_test(cx, |_| {});
8213
8214 let mut cx = EditorTestContext::new(cx).await;
8215 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8216
8217 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8218 .unwrap();
8219 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8220
8221 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8222 .unwrap();
8223 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8224
8225 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8226 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8227
8228 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8229 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8230
8231 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8232 .unwrap();
8233 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8234
8235 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8236 .unwrap();
8237 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8238
8239 // Test selection direction should be preserved
8240 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8241
8242 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8243 .unwrap();
8244 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8245}
8246
8247#[gpui::test]
8248async fn test_select_all_matches(cx: &mut TestAppContext) {
8249 init_test(cx, |_| {});
8250
8251 let mut cx = EditorTestContext::new(cx).await;
8252
8253 // Test caret-only selections
8254 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8255 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8256 .unwrap();
8257 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8258
8259 // Test left-to-right selections
8260 cx.set_state("abc\n«abcˇ»\nabc");
8261 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8262 .unwrap();
8263 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8264
8265 // Test right-to-left selections
8266 cx.set_state("abc\n«ˇabc»\nabc");
8267 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8268 .unwrap();
8269 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8270
8271 // Test selecting whitespace with caret selection
8272 cx.set_state("abc\nˇ abc\nabc");
8273 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8274 .unwrap();
8275 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8276
8277 // Test selecting whitespace with left-to-right selection
8278 cx.set_state("abc\n«ˇ »abc\nabc");
8279 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8280 .unwrap();
8281 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8282
8283 // Test no matches with right-to-left selection
8284 cx.set_state("abc\n« ˇ»abc\nabc");
8285 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8286 .unwrap();
8287 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8288
8289 // Test with a single word and clip_at_line_ends=true (#29823)
8290 cx.set_state("aˇbc");
8291 cx.update_editor(|e, window, cx| {
8292 e.set_clip_at_line_ends(true, cx);
8293 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8294 e.set_clip_at_line_ends(false, cx);
8295 });
8296 cx.assert_editor_state("«abcˇ»");
8297}
8298
8299#[gpui::test]
8300async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8301 init_test(cx, |_| {});
8302
8303 let mut cx = EditorTestContext::new(cx).await;
8304
8305 let large_body_1 = "\nd".repeat(200);
8306 let large_body_2 = "\ne".repeat(200);
8307
8308 cx.set_state(&format!(
8309 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8310 ));
8311 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8312 let scroll_position = editor.scroll_position(cx);
8313 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8314 scroll_position
8315 });
8316
8317 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8318 .unwrap();
8319 cx.assert_editor_state(&format!(
8320 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8321 ));
8322 let scroll_position_after_selection =
8323 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8324 assert_eq!(
8325 initial_scroll_position, scroll_position_after_selection,
8326 "Scroll position should not change after selecting all matches"
8327 );
8328}
8329
8330#[gpui::test]
8331async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8332 init_test(cx, |_| {});
8333
8334 let mut cx = EditorLspTestContext::new_rust(
8335 lsp::ServerCapabilities {
8336 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8337 ..Default::default()
8338 },
8339 cx,
8340 )
8341 .await;
8342
8343 cx.set_state(indoc! {"
8344 line 1
8345 line 2
8346 linˇe 3
8347 line 4
8348 line 5
8349 "});
8350
8351 // Make an edit
8352 cx.update_editor(|editor, window, cx| {
8353 editor.handle_input("X", window, cx);
8354 });
8355
8356 // Move cursor to a different position
8357 cx.update_editor(|editor, window, cx| {
8358 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8359 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8360 });
8361 });
8362
8363 cx.assert_editor_state(indoc! {"
8364 line 1
8365 line 2
8366 linXe 3
8367 line 4
8368 liˇne 5
8369 "});
8370
8371 cx.lsp
8372 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8373 Ok(Some(vec![lsp::TextEdit::new(
8374 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8375 "PREFIX ".to_string(),
8376 )]))
8377 });
8378
8379 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8380 .unwrap()
8381 .await
8382 .unwrap();
8383
8384 cx.assert_editor_state(indoc! {"
8385 PREFIX line 1
8386 line 2
8387 linXe 3
8388 line 4
8389 liˇne 5
8390 "});
8391
8392 // Undo formatting
8393 cx.update_editor(|editor, window, cx| {
8394 editor.undo(&Default::default(), window, cx);
8395 });
8396
8397 // Verify cursor moved back to position after edit
8398 cx.assert_editor_state(indoc! {"
8399 line 1
8400 line 2
8401 linXˇe 3
8402 line 4
8403 line 5
8404 "});
8405}
8406
8407#[gpui::test]
8408async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8409 init_test(cx, |_| {});
8410
8411 let mut cx = EditorTestContext::new(cx).await;
8412
8413 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8414 cx.update_editor(|editor, window, cx| {
8415 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8416 });
8417
8418 cx.set_state(indoc! {"
8419 line 1
8420 line 2
8421 linˇe 3
8422 line 4
8423 line 5
8424 line 6
8425 line 7
8426 line 8
8427 line 9
8428 line 10
8429 "});
8430
8431 let snapshot = cx.buffer_snapshot();
8432 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8433
8434 cx.update(|_, cx| {
8435 provider.update(cx, |provider, _| {
8436 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8437 id: None,
8438 edits: vec![(edit_position..edit_position, "X".into())],
8439 edit_preview: None,
8440 }))
8441 })
8442 });
8443
8444 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8445 cx.update_editor(|editor, window, cx| {
8446 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8447 });
8448
8449 cx.assert_editor_state(indoc! {"
8450 line 1
8451 line 2
8452 lineXˇ 3
8453 line 4
8454 line 5
8455 line 6
8456 line 7
8457 line 8
8458 line 9
8459 line 10
8460 "});
8461
8462 cx.update_editor(|editor, window, cx| {
8463 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8464 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8465 });
8466 });
8467
8468 cx.assert_editor_state(indoc! {"
8469 line 1
8470 line 2
8471 lineX 3
8472 line 4
8473 line 5
8474 line 6
8475 line 7
8476 line 8
8477 line 9
8478 liˇne 10
8479 "});
8480
8481 cx.update_editor(|editor, window, cx| {
8482 editor.undo(&Default::default(), window, cx);
8483 });
8484
8485 cx.assert_editor_state(indoc! {"
8486 line 1
8487 line 2
8488 lineˇ 3
8489 line 4
8490 line 5
8491 line 6
8492 line 7
8493 line 8
8494 line 9
8495 line 10
8496 "});
8497}
8498
8499#[gpui::test]
8500async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8501 init_test(cx, |_| {});
8502
8503 let mut cx = EditorTestContext::new(cx).await;
8504 cx.set_state(
8505 r#"let foo = 2;
8506lˇet foo = 2;
8507let fooˇ = 2;
8508let foo = 2;
8509let foo = ˇ2;"#,
8510 );
8511
8512 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8513 .unwrap();
8514 cx.assert_editor_state(
8515 r#"let foo = 2;
8516«letˇ» foo = 2;
8517let «fooˇ» = 2;
8518let foo = 2;
8519let foo = «2ˇ»;"#,
8520 );
8521
8522 // noop for multiple selections with different contents
8523 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8524 .unwrap();
8525 cx.assert_editor_state(
8526 r#"let foo = 2;
8527«letˇ» foo = 2;
8528let «fooˇ» = 2;
8529let foo = 2;
8530let foo = «2ˇ»;"#,
8531 );
8532
8533 // Test last selection direction should be preserved
8534 cx.set_state(
8535 r#"let foo = 2;
8536let foo = 2;
8537let «fooˇ» = 2;
8538let «ˇfoo» = 2;
8539let foo = 2;"#,
8540 );
8541
8542 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8543 .unwrap();
8544 cx.assert_editor_state(
8545 r#"let foo = 2;
8546let foo = 2;
8547let «fooˇ» = 2;
8548let «ˇfoo» = 2;
8549let «ˇfoo» = 2;"#,
8550 );
8551}
8552
8553#[gpui::test]
8554async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8555 init_test(cx, |_| {});
8556
8557 let mut cx =
8558 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8559
8560 cx.assert_editor_state(indoc! {"
8561 ˇbbb
8562 ccc
8563
8564 bbb
8565 ccc
8566 "});
8567 cx.dispatch_action(SelectPrevious::default());
8568 cx.assert_editor_state(indoc! {"
8569 «bbbˇ»
8570 ccc
8571
8572 bbb
8573 ccc
8574 "});
8575 cx.dispatch_action(SelectPrevious::default());
8576 cx.assert_editor_state(indoc! {"
8577 «bbbˇ»
8578 ccc
8579
8580 «bbbˇ»
8581 ccc
8582 "});
8583}
8584
8585#[gpui::test]
8586async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8587 init_test(cx, |_| {});
8588
8589 let mut cx = EditorTestContext::new(cx).await;
8590 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8591
8592 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8593 .unwrap();
8594 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8595
8596 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8597 .unwrap();
8598 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8599
8600 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8601 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8602
8603 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8604 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8605
8606 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8607 .unwrap();
8608 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8609
8610 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8611 .unwrap();
8612 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8613}
8614
8615#[gpui::test]
8616async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8617 init_test(cx, |_| {});
8618
8619 let mut cx = EditorTestContext::new(cx).await;
8620 cx.set_state("aˇ");
8621
8622 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8623 .unwrap();
8624 cx.assert_editor_state("«aˇ»");
8625 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8626 .unwrap();
8627 cx.assert_editor_state("«aˇ»");
8628}
8629
8630#[gpui::test]
8631async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8632 init_test(cx, |_| {});
8633
8634 let mut cx = EditorTestContext::new(cx).await;
8635 cx.set_state(
8636 r#"let foo = 2;
8637lˇet foo = 2;
8638let fooˇ = 2;
8639let foo = 2;
8640let foo = ˇ2;"#,
8641 );
8642
8643 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8644 .unwrap();
8645 cx.assert_editor_state(
8646 r#"let foo = 2;
8647«letˇ» foo = 2;
8648let «fooˇ» = 2;
8649let foo = 2;
8650let foo = «2ˇ»;"#,
8651 );
8652
8653 // noop for multiple selections with different contents
8654 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8655 .unwrap();
8656 cx.assert_editor_state(
8657 r#"let foo = 2;
8658«letˇ» foo = 2;
8659let «fooˇ» = 2;
8660let foo = 2;
8661let foo = «2ˇ»;"#,
8662 );
8663}
8664
8665#[gpui::test]
8666async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8667 init_test(cx, |_| {});
8668
8669 let mut cx = EditorTestContext::new(cx).await;
8670 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8671
8672 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8673 .unwrap();
8674 // selection direction is preserved
8675 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8676
8677 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8678 .unwrap();
8679 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8680
8681 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8682 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8683
8684 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8685 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8686
8687 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8688 .unwrap();
8689 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8690
8691 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8692 .unwrap();
8693 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8694}
8695
8696#[gpui::test]
8697async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8698 init_test(cx, |_| {});
8699
8700 let language = Arc::new(Language::new(
8701 LanguageConfig::default(),
8702 Some(tree_sitter_rust::LANGUAGE.into()),
8703 ));
8704
8705 let text = r#"
8706 use mod1::mod2::{mod3, mod4};
8707
8708 fn fn_1(param1: bool, param2: &str) {
8709 let var1 = "text";
8710 }
8711 "#
8712 .unindent();
8713
8714 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8715 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8716 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8717
8718 editor
8719 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8720 .await;
8721
8722 editor.update_in(cx, |editor, window, cx| {
8723 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8724 s.select_display_ranges([
8725 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8726 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8727 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8728 ]);
8729 });
8730 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8731 });
8732 editor.update(cx, |editor, cx| {
8733 assert_text_with_selections(
8734 editor,
8735 indoc! {r#"
8736 use mod1::mod2::{mod3, «mod4ˇ»};
8737
8738 fn fn_1«ˇ(param1: bool, param2: &str)» {
8739 let var1 = "«ˇtext»";
8740 }
8741 "#},
8742 cx,
8743 );
8744 });
8745
8746 editor.update_in(cx, |editor, window, cx| {
8747 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8748 });
8749 editor.update(cx, |editor, cx| {
8750 assert_text_with_selections(
8751 editor,
8752 indoc! {r#"
8753 use mod1::mod2::«{mod3, mod4}ˇ»;
8754
8755 «ˇfn fn_1(param1: bool, param2: &str) {
8756 let var1 = "text";
8757 }»
8758 "#},
8759 cx,
8760 );
8761 });
8762
8763 editor.update_in(cx, |editor, window, cx| {
8764 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8765 });
8766 assert_eq!(
8767 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8768 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8769 );
8770
8771 // Trying to expand the selected syntax node one more time has no effect.
8772 editor.update_in(cx, |editor, window, cx| {
8773 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8774 });
8775 assert_eq!(
8776 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8777 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8778 );
8779
8780 editor.update_in(cx, |editor, window, cx| {
8781 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8782 });
8783 editor.update(cx, |editor, cx| {
8784 assert_text_with_selections(
8785 editor,
8786 indoc! {r#"
8787 use mod1::mod2::«{mod3, mod4}ˇ»;
8788
8789 «ˇfn fn_1(param1: bool, param2: &str) {
8790 let var1 = "text";
8791 }»
8792 "#},
8793 cx,
8794 );
8795 });
8796
8797 editor.update_in(cx, |editor, window, cx| {
8798 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8799 });
8800 editor.update(cx, |editor, cx| {
8801 assert_text_with_selections(
8802 editor,
8803 indoc! {r#"
8804 use mod1::mod2::{mod3, «mod4ˇ»};
8805
8806 fn fn_1«ˇ(param1: bool, param2: &str)» {
8807 let var1 = "«ˇtext»";
8808 }
8809 "#},
8810 cx,
8811 );
8812 });
8813
8814 editor.update_in(cx, |editor, window, cx| {
8815 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8816 });
8817 editor.update(cx, |editor, cx| {
8818 assert_text_with_selections(
8819 editor,
8820 indoc! {r#"
8821 use mod1::mod2::{mod3, moˇd4};
8822
8823 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8824 let var1 = "teˇxt";
8825 }
8826 "#},
8827 cx,
8828 );
8829 });
8830
8831 // Trying to shrink the selected syntax node one more time has no effect.
8832 editor.update_in(cx, |editor, window, cx| {
8833 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8834 });
8835 editor.update_in(cx, |editor, _, cx| {
8836 assert_text_with_selections(
8837 editor,
8838 indoc! {r#"
8839 use mod1::mod2::{mod3, moˇd4};
8840
8841 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8842 let var1 = "teˇxt";
8843 }
8844 "#},
8845 cx,
8846 );
8847 });
8848
8849 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8850 // a fold.
8851 editor.update_in(cx, |editor, window, cx| {
8852 editor.fold_creases(
8853 vec![
8854 Crease::simple(
8855 Point::new(0, 21)..Point::new(0, 24),
8856 FoldPlaceholder::test(),
8857 ),
8858 Crease::simple(
8859 Point::new(3, 20)..Point::new(3, 22),
8860 FoldPlaceholder::test(),
8861 ),
8862 ],
8863 true,
8864 window,
8865 cx,
8866 );
8867 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8868 });
8869 editor.update(cx, |editor, cx| {
8870 assert_text_with_selections(
8871 editor,
8872 indoc! {r#"
8873 use mod1::mod2::«{mod3, mod4}ˇ»;
8874
8875 fn fn_1«ˇ(param1: bool, param2: &str)» {
8876 let var1 = "«ˇtext»";
8877 }
8878 "#},
8879 cx,
8880 );
8881 });
8882}
8883
8884#[gpui::test]
8885async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8886 init_test(cx, |_| {});
8887
8888 let language = Arc::new(Language::new(
8889 LanguageConfig::default(),
8890 Some(tree_sitter_rust::LANGUAGE.into()),
8891 ));
8892
8893 let text = "let a = 2;";
8894
8895 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8896 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8897 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8898
8899 editor
8900 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8901 .await;
8902
8903 // Test case 1: Cursor at end of word
8904 editor.update_in(cx, |editor, window, cx| {
8905 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8906 s.select_display_ranges([
8907 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
8908 ]);
8909 });
8910 });
8911 editor.update(cx, |editor, cx| {
8912 assert_text_with_selections(editor, "let aˇ = 2;", cx);
8913 });
8914 editor.update_in(cx, |editor, window, cx| {
8915 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8916 });
8917 editor.update(cx, |editor, cx| {
8918 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
8919 });
8920 editor.update_in(cx, |editor, window, cx| {
8921 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8922 });
8923 editor.update(cx, |editor, cx| {
8924 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8925 });
8926
8927 // Test case 2: Cursor at end of statement
8928 editor.update_in(cx, |editor, window, cx| {
8929 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8930 s.select_display_ranges([
8931 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
8932 ]);
8933 });
8934 });
8935 editor.update(cx, |editor, cx| {
8936 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
8937 });
8938 editor.update_in(cx, |editor, window, cx| {
8939 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8940 });
8941 editor.update(cx, |editor, cx| {
8942 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8943 });
8944}
8945
8946#[gpui::test]
8947async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
8948 init_test(cx, |_| {});
8949
8950 let language = Arc::new(Language::new(
8951 LanguageConfig {
8952 name: "JavaScript".into(),
8953 ..Default::default()
8954 },
8955 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8956 ));
8957
8958 let text = r#"
8959 let a = {
8960 key: "value",
8961 };
8962 "#
8963 .unindent();
8964
8965 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8966 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8967 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8968
8969 editor
8970 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8971 .await;
8972
8973 // Test case 1: Cursor after '{'
8974 editor.update_in(cx, |editor, window, cx| {
8975 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8976 s.select_display_ranges([
8977 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
8978 ]);
8979 });
8980 });
8981 editor.update(cx, |editor, cx| {
8982 assert_text_with_selections(
8983 editor,
8984 indoc! {r#"
8985 let a = {ˇ
8986 key: "value",
8987 };
8988 "#},
8989 cx,
8990 );
8991 });
8992 editor.update_in(cx, |editor, window, cx| {
8993 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8994 });
8995 editor.update(cx, |editor, cx| {
8996 assert_text_with_selections(
8997 editor,
8998 indoc! {r#"
8999 let a = «ˇ{
9000 key: "value",
9001 }»;
9002 "#},
9003 cx,
9004 );
9005 });
9006
9007 // Test case 2: Cursor after ':'
9008 editor.update_in(cx, |editor, window, cx| {
9009 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9010 s.select_display_ranges([
9011 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9012 ]);
9013 });
9014 });
9015 editor.update(cx, |editor, cx| {
9016 assert_text_with_selections(
9017 editor,
9018 indoc! {r#"
9019 let a = {
9020 key:ˇ "value",
9021 };
9022 "#},
9023 cx,
9024 );
9025 });
9026 editor.update_in(cx, |editor, window, cx| {
9027 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9028 });
9029 editor.update(cx, |editor, cx| {
9030 assert_text_with_selections(
9031 editor,
9032 indoc! {r#"
9033 let a = {
9034 «ˇkey: "value"»,
9035 };
9036 "#},
9037 cx,
9038 );
9039 });
9040 editor.update_in(cx, |editor, window, cx| {
9041 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9042 });
9043 editor.update(cx, |editor, cx| {
9044 assert_text_with_selections(
9045 editor,
9046 indoc! {r#"
9047 let a = «ˇ{
9048 key: "value",
9049 }»;
9050 "#},
9051 cx,
9052 );
9053 });
9054
9055 // Test case 3: Cursor after ','
9056 editor.update_in(cx, |editor, window, cx| {
9057 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9058 s.select_display_ranges([
9059 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9060 ]);
9061 });
9062 });
9063 editor.update(cx, |editor, cx| {
9064 assert_text_with_selections(
9065 editor,
9066 indoc! {r#"
9067 let a = {
9068 key: "value",ˇ
9069 };
9070 "#},
9071 cx,
9072 );
9073 });
9074 editor.update_in(cx, |editor, window, cx| {
9075 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9076 });
9077 editor.update(cx, |editor, cx| {
9078 assert_text_with_selections(
9079 editor,
9080 indoc! {r#"
9081 let a = «ˇ{
9082 key: "value",
9083 }»;
9084 "#},
9085 cx,
9086 );
9087 });
9088
9089 // Test case 4: Cursor after ';'
9090 editor.update_in(cx, |editor, window, cx| {
9091 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9092 s.select_display_ranges([
9093 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9094 ]);
9095 });
9096 });
9097 editor.update(cx, |editor, cx| {
9098 assert_text_with_selections(
9099 editor,
9100 indoc! {r#"
9101 let a = {
9102 key: "value",
9103 };ˇ
9104 "#},
9105 cx,
9106 );
9107 });
9108 editor.update_in(cx, |editor, window, cx| {
9109 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9110 });
9111 editor.update(cx, |editor, cx| {
9112 assert_text_with_selections(
9113 editor,
9114 indoc! {r#"
9115 «ˇlet a = {
9116 key: "value",
9117 };
9118 »"#},
9119 cx,
9120 );
9121 });
9122}
9123
9124#[gpui::test]
9125async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9126 init_test(cx, |_| {});
9127
9128 let language = Arc::new(Language::new(
9129 LanguageConfig::default(),
9130 Some(tree_sitter_rust::LANGUAGE.into()),
9131 ));
9132
9133 let text = r#"
9134 use mod1::mod2::{mod3, mod4};
9135
9136 fn fn_1(param1: bool, param2: &str) {
9137 let var1 = "hello world";
9138 }
9139 "#
9140 .unindent();
9141
9142 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9143 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9144 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9145
9146 editor
9147 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9148 .await;
9149
9150 // Test 1: Cursor on a letter of a string word
9151 editor.update_in(cx, |editor, window, cx| {
9152 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9153 s.select_display_ranges([
9154 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9155 ]);
9156 });
9157 });
9158 editor.update_in(cx, |editor, window, cx| {
9159 assert_text_with_selections(
9160 editor,
9161 indoc! {r#"
9162 use mod1::mod2::{mod3, mod4};
9163
9164 fn fn_1(param1: bool, param2: &str) {
9165 let var1 = "hˇello world";
9166 }
9167 "#},
9168 cx,
9169 );
9170 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9171 assert_text_with_selections(
9172 editor,
9173 indoc! {r#"
9174 use mod1::mod2::{mod3, mod4};
9175
9176 fn fn_1(param1: bool, param2: &str) {
9177 let var1 = "«ˇhello» world";
9178 }
9179 "#},
9180 cx,
9181 );
9182 });
9183
9184 // Test 2: Partial selection within a word
9185 editor.update_in(cx, |editor, window, cx| {
9186 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9187 s.select_display_ranges([
9188 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9189 ]);
9190 });
9191 });
9192 editor.update_in(cx, |editor, window, cx| {
9193 assert_text_with_selections(
9194 editor,
9195 indoc! {r#"
9196 use mod1::mod2::{mod3, mod4};
9197
9198 fn fn_1(param1: bool, param2: &str) {
9199 let var1 = "h«elˇ»lo world";
9200 }
9201 "#},
9202 cx,
9203 );
9204 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9205 assert_text_with_selections(
9206 editor,
9207 indoc! {r#"
9208 use mod1::mod2::{mod3, mod4};
9209
9210 fn fn_1(param1: bool, param2: &str) {
9211 let var1 = "«ˇhello» world";
9212 }
9213 "#},
9214 cx,
9215 );
9216 });
9217
9218 // Test 3: Complete word already selected
9219 editor.update_in(cx, |editor, window, cx| {
9220 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9221 s.select_display_ranges([
9222 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9223 ]);
9224 });
9225 });
9226 editor.update_in(cx, |editor, window, cx| {
9227 assert_text_with_selections(
9228 editor,
9229 indoc! {r#"
9230 use mod1::mod2::{mod3, mod4};
9231
9232 fn fn_1(param1: bool, param2: &str) {
9233 let var1 = "«helloˇ» world";
9234 }
9235 "#},
9236 cx,
9237 );
9238 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9239 assert_text_with_selections(
9240 editor,
9241 indoc! {r#"
9242 use mod1::mod2::{mod3, mod4};
9243
9244 fn fn_1(param1: bool, param2: &str) {
9245 let var1 = "«hello worldˇ»";
9246 }
9247 "#},
9248 cx,
9249 );
9250 });
9251
9252 // Test 4: Selection spanning across words
9253 editor.update_in(cx, |editor, window, cx| {
9254 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9255 s.select_display_ranges([
9256 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9257 ]);
9258 });
9259 });
9260 editor.update_in(cx, |editor, window, cx| {
9261 assert_text_with_selections(
9262 editor,
9263 indoc! {r#"
9264 use mod1::mod2::{mod3, mod4};
9265
9266 fn fn_1(param1: bool, param2: &str) {
9267 let var1 = "hel«lo woˇ»rld";
9268 }
9269 "#},
9270 cx,
9271 );
9272 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9273 assert_text_with_selections(
9274 editor,
9275 indoc! {r#"
9276 use mod1::mod2::{mod3, mod4};
9277
9278 fn fn_1(param1: bool, param2: &str) {
9279 let var1 = "«ˇhello world»";
9280 }
9281 "#},
9282 cx,
9283 );
9284 });
9285
9286 // Test 5: Expansion beyond string
9287 editor.update_in(cx, |editor, window, cx| {
9288 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9289 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9290 assert_text_with_selections(
9291 editor,
9292 indoc! {r#"
9293 use mod1::mod2::{mod3, mod4};
9294
9295 fn fn_1(param1: bool, param2: &str) {
9296 «ˇlet var1 = "hello world";»
9297 }
9298 "#},
9299 cx,
9300 );
9301 });
9302}
9303
9304#[gpui::test]
9305async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9306 init_test(cx, |_| {});
9307
9308 let mut cx = EditorTestContext::new(cx).await;
9309
9310 let language = Arc::new(Language::new(
9311 LanguageConfig::default(),
9312 Some(tree_sitter_rust::LANGUAGE.into()),
9313 ));
9314
9315 cx.update_buffer(|buffer, cx| {
9316 buffer.set_language(Some(language), cx);
9317 });
9318
9319 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9320 cx.update_editor(|editor, window, cx| {
9321 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9322 });
9323
9324 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9325
9326 cx.set_state(indoc! { r#"fn a() {
9327 // what
9328 // a
9329 // ˇlong
9330 // method
9331 // I
9332 // sure
9333 // hope
9334 // it
9335 // works
9336 }"# });
9337
9338 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9339 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9340 cx.update(|_, cx| {
9341 multi_buffer.update(cx, |multi_buffer, cx| {
9342 multi_buffer.set_excerpts_for_path(
9343 PathKey::for_buffer(&buffer, cx),
9344 buffer,
9345 [Point::new(1, 0)..Point::new(1, 0)],
9346 3,
9347 cx,
9348 );
9349 });
9350 });
9351
9352 let editor2 = cx.new_window_entity(|window, cx| {
9353 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9354 });
9355
9356 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9357 cx.update_editor(|editor, window, cx| {
9358 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9359 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9360 })
9361 });
9362
9363 cx.assert_editor_state(indoc! { "
9364 fn a() {
9365 // what
9366 // a
9367 ˇ // long
9368 // method"});
9369
9370 cx.update_editor(|editor, window, cx| {
9371 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9372 });
9373
9374 // Although we could potentially make the action work when the syntax node
9375 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9376 // did. Maybe we could also expand the excerpt to contain the range?
9377 cx.assert_editor_state(indoc! { "
9378 fn a() {
9379 // what
9380 // a
9381 ˇ // long
9382 // method"});
9383}
9384
9385#[gpui::test]
9386async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9387 init_test(cx, |_| {});
9388
9389 let base_text = r#"
9390 impl A {
9391 // this is an uncommitted comment
9392
9393 fn b() {
9394 c();
9395 }
9396
9397 // this is another uncommitted comment
9398
9399 fn d() {
9400 // e
9401 // f
9402 }
9403 }
9404
9405 fn g() {
9406 // h
9407 }
9408 "#
9409 .unindent();
9410
9411 let text = r#"
9412 ˇimpl A {
9413
9414 fn b() {
9415 c();
9416 }
9417
9418 fn d() {
9419 // e
9420 // f
9421 }
9422 }
9423
9424 fn g() {
9425 // h
9426 }
9427 "#
9428 .unindent();
9429
9430 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9431 cx.set_state(&text);
9432 cx.set_head_text(&base_text);
9433 cx.update_editor(|editor, window, cx| {
9434 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9435 });
9436
9437 cx.assert_state_with_diff(
9438 "
9439 ˇimpl A {
9440 - // this is an uncommitted comment
9441
9442 fn b() {
9443 c();
9444 }
9445
9446 - // this is another uncommitted comment
9447 -
9448 fn d() {
9449 // e
9450 // f
9451 }
9452 }
9453
9454 fn g() {
9455 // h
9456 }
9457 "
9458 .unindent(),
9459 );
9460
9461 let expected_display_text = "
9462 impl A {
9463 // this is an uncommitted comment
9464
9465 fn b() {
9466 ⋯
9467 }
9468
9469 // this is another uncommitted comment
9470
9471 fn d() {
9472 ⋯
9473 }
9474 }
9475
9476 fn g() {
9477 ⋯
9478 }
9479 "
9480 .unindent();
9481
9482 cx.update_editor(|editor, window, cx| {
9483 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9484 assert_eq!(editor.display_text(cx), expected_display_text);
9485 });
9486}
9487
9488#[gpui::test]
9489async fn test_autoindent(cx: &mut TestAppContext) {
9490 init_test(cx, |_| {});
9491
9492 let language = Arc::new(
9493 Language::new(
9494 LanguageConfig {
9495 brackets: BracketPairConfig {
9496 pairs: vec![
9497 BracketPair {
9498 start: "{".to_string(),
9499 end: "}".to_string(),
9500 close: false,
9501 surround: false,
9502 newline: true,
9503 },
9504 BracketPair {
9505 start: "(".to_string(),
9506 end: ")".to_string(),
9507 close: false,
9508 surround: false,
9509 newline: true,
9510 },
9511 ],
9512 ..Default::default()
9513 },
9514 ..Default::default()
9515 },
9516 Some(tree_sitter_rust::LANGUAGE.into()),
9517 )
9518 .with_indents_query(
9519 r#"
9520 (_ "(" ")" @end) @indent
9521 (_ "{" "}" @end) @indent
9522 "#,
9523 )
9524 .unwrap(),
9525 );
9526
9527 let text = "fn a() {}";
9528
9529 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9530 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9531 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9532 editor
9533 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9534 .await;
9535
9536 editor.update_in(cx, |editor, window, cx| {
9537 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9538 s.select_ranges([5..5, 8..8, 9..9])
9539 });
9540 editor.newline(&Newline, window, cx);
9541 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9542 assert_eq!(
9543 editor.selections.ranges(cx),
9544 &[
9545 Point::new(1, 4)..Point::new(1, 4),
9546 Point::new(3, 4)..Point::new(3, 4),
9547 Point::new(5, 0)..Point::new(5, 0)
9548 ]
9549 );
9550 });
9551}
9552
9553#[gpui::test]
9554async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9555 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9556
9557 let language = Arc::new(
9558 Language::new(
9559 LanguageConfig {
9560 brackets: BracketPairConfig {
9561 pairs: vec![
9562 BracketPair {
9563 start: "{".to_string(),
9564 end: "}".to_string(),
9565 close: false,
9566 surround: false,
9567 newline: true,
9568 },
9569 BracketPair {
9570 start: "(".to_string(),
9571 end: ")".to_string(),
9572 close: false,
9573 surround: false,
9574 newline: true,
9575 },
9576 ],
9577 ..Default::default()
9578 },
9579 ..Default::default()
9580 },
9581 Some(tree_sitter_rust::LANGUAGE.into()),
9582 )
9583 .with_indents_query(
9584 r#"
9585 (_ "(" ")" @end) @indent
9586 (_ "{" "}" @end) @indent
9587 "#,
9588 )
9589 .unwrap(),
9590 );
9591
9592 let text = "fn a() {}";
9593
9594 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9595 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9596 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9597 editor
9598 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9599 .await;
9600
9601 editor.update_in(cx, |editor, window, cx| {
9602 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9603 s.select_ranges([5..5, 8..8, 9..9])
9604 });
9605 editor.newline(&Newline, window, cx);
9606 assert_eq!(
9607 editor.text(cx),
9608 indoc!(
9609 "
9610 fn a(
9611
9612 ) {
9613
9614 }
9615 "
9616 )
9617 );
9618 assert_eq!(
9619 editor.selections.ranges(cx),
9620 &[
9621 Point::new(1, 0)..Point::new(1, 0),
9622 Point::new(3, 0)..Point::new(3, 0),
9623 Point::new(5, 0)..Point::new(5, 0)
9624 ]
9625 );
9626 });
9627}
9628
9629#[gpui::test]
9630async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9631 init_test(cx, |settings| {
9632 settings.defaults.auto_indent = Some(true);
9633 settings.languages.0.insert(
9634 "python".into(),
9635 LanguageSettingsContent {
9636 auto_indent: Some(false),
9637 ..Default::default()
9638 },
9639 );
9640 });
9641
9642 let mut cx = EditorTestContext::new(cx).await;
9643
9644 let injected_language = Arc::new(
9645 Language::new(
9646 LanguageConfig {
9647 brackets: BracketPairConfig {
9648 pairs: vec![
9649 BracketPair {
9650 start: "{".to_string(),
9651 end: "}".to_string(),
9652 close: false,
9653 surround: false,
9654 newline: true,
9655 },
9656 BracketPair {
9657 start: "(".to_string(),
9658 end: ")".to_string(),
9659 close: true,
9660 surround: false,
9661 newline: true,
9662 },
9663 ],
9664 ..Default::default()
9665 },
9666 name: "python".into(),
9667 ..Default::default()
9668 },
9669 Some(tree_sitter_python::LANGUAGE.into()),
9670 )
9671 .with_indents_query(
9672 r#"
9673 (_ "(" ")" @end) @indent
9674 (_ "{" "}" @end) @indent
9675 "#,
9676 )
9677 .unwrap(),
9678 );
9679
9680 let language = Arc::new(
9681 Language::new(
9682 LanguageConfig {
9683 brackets: BracketPairConfig {
9684 pairs: vec![
9685 BracketPair {
9686 start: "{".to_string(),
9687 end: "}".to_string(),
9688 close: false,
9689 surround: false,
9690 newline: true,
9691 },
9692 BracketPair {
9693 start: "(".to_string(),
9694 end: ")".to_string(),
9695 close: true,
9696 surround: false,
9697 newline: true,
9698 },
9699 ],
9700 ..Default::default()
9701 },
9702 name: LanguageName::new("rust"),
9703 ..Default::default()
9704 },
9705 Some(tree_sitter_rust::LANGUAGE.into()),
9706 )
9707 .with_indents_query(
9708 r#"
9709 (_ "(" ")" @end) @indent
9710 (_ "{" "}" @end) @indent
9711 "#,
9712 )
9713 .unwrap()
9714 .with_injection_query(
9715 r#"
9716 (macro_invocation
9717 macro: (identifier) @_macro_name
9718 (token_tree) @injection.content
9719 (#set! injection.language "python"))
9720 "#,
9721 )
9722 .unwrap(),
9723 );
9724
9725 cx.language_registry().add(injected_language);
9726 cx.language_registry().add(language.clone());
9727
9728 cx.update_buffer(|buffer, cx| {
9729 buffer.set_language(Some(language), cx);
9730 });
9731
9732 cx.set_state(r#"struct A {ˇ}"#);
9733
9734 cx.update_editor(|editor, window, cx| {
9735 editor.newline(&Default::default(), window, cx);
9736 });
9737
9738 cx.assert_editor_state(indoc!(
9739 "struct A {
9740 ˇ
9741 }"
9742 ));
9743
9744 cx.set_state(r#"select_biased!(ˇ)"#);
9745
9746 cx.update_editor(|editor, window, cx| {
9747 editor.newline(&Default::default(), window, cx);
9748 editor.handle_input("def ", window, cx);
9749 editor.handle_input("(", window, cx);
9750 editor.newline(&Default::default(), window, cx);
9751 editor.handle_input("a", window, cx);
9752 });
9753
9754 cx.assert_editor_state(indoc!(
9755 "select_biased!(
9756 def (
9757 aˇ
9758 )
9759 )"
9760 ));
9761}
9762
9763#[gpui::test]
9764async fn test_autoindent_selections(cx: &mut TestAppContext) {
9765 init_test(cx, |_| {});
9766
9767 {
9768 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9769 cx.set_state(indoc! {"
9770 impl A {
9771
9772 fn b() {}
9773
9774 «fn c() {
9775
9776 }ˇ»
9777 }
9778 "});
9779
9780 cx.update_editor(|editor, window, cx| {
9781 editor.autoindent(&Default::default(), window, cx);
9782 });
9783
9784 cx.assert_editor_state(indoc! {"
9785 impl A {
9786
9787 fn b() {}
9788
9789 «fn c() {
9790
9791 }ˇ»
9792 }
9793 "});
9794 }
9795
9796 {
9797 let mut cx = EditorTestContext::new_multibuffer(
9798 cx,
9799 [indoc! { "
9800 impl A {
9801 «
9802 // a
9803 fn b(){}
9804 »
9805 «
9806 }
9807 fn c(){}
9808 »
9809 "}],
9810 );
9811
9812 let buffer = cx.update_editor(|editor, _, cx| {
9813 let buffer = editor.buffer().update(cx, |buffer, _| {
9814 buffer.all_buffers().iter().next().unwrap().clone()
9815 });
9816 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9817 buffer
9818 });
9819
9820 cx.run_until_parked();
9821 cx.update_editor(|editor, window, cx| {
9822 editor.select_all(&Default::default(), window, cx);
9823 editor.autoindent(&Default::default(), window, cx)
9824 });
9825 cx.run_until_parked();
9826
9827 cx.update(|_, cx| {
9828 assert_eq!(
9829 buffer.read(cx).text(),
9830 indoc! { "
9831 impl A {
9832
9833 // a
9834 fn b(){}
9835
9836
9837 }
9838 fn c(){}
9839
9840 " }
9841 )
9842 });
9843 }
9844}
9845
9846#[gpui::test]
9847async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9848 init_test(cx, |_| {});
9849
9850 let mut cx = EditorTestContext::new(cx).await;
9851
9852 let language = Arc::new(Language::new(
9853 LanguageConfig {
9854 brackets: BracketPairConfig {
9855 pairs: vec![
9856 BracketPair {
9857 start: "{".to_string(),
9858 end: "}".to_string(),
9859 close: true,
9860 surround: true,
9861 newline: true,
9862 },
9863 BracketPair {
9864 start: "(".to_string(),
9865 end: ")".to_string(),
9866 close: true,
9867 surround: true,
9868 newline: true,
9869 },
9870 BracketPair {
9871 start: "/*".to_string(),
9872 end: " */".to_string(),
9873 close: true,
9874 surround: true,
9875 newline: true,
9876 },
9877 BracketPair {
9878 start: "[".to_string(),
9879 end: "]".to_string(),
9880 close: false,
9881 surround: false,
9882 newline: true,
9883 },
9884 BracketPair {
9885 start: "\"".to_string(),
9886 end: "\"".to_string(),
9887 close: true,
9888 surround: true,
9889 newline: false,
9890 },
9891 BracketPair {
9892 start: "<".to_string(),
9893 end: ">".to_string(),
9894 close: false,
9895 surround: true,
9896 newline: true,
9897 },
9898 ],
9899 ..Default::default()
9900 },
9901 autoclose_before: "})]".to_string(),
9902 ..Default::default()
9903 },
9904 Some(tree_sitter_rust::LANGUAGE.into()),
9905 ));
9906
9907 cx.language_registry().add(language.clone());
9908 cx.update_buffer(|buffer, cx| {
9909 buffer.set_language(Some(language), cx);
9910 });
9911
9912 cx.set_state(
9913 &r#"
9914 🏀ˇ
9915 εˇ
9916 ❤️ˇ
9917 "#
9918 .unindent(),
9919 );
9920
9921 // autoclose multiple nested brackets at multiple cursors
9922 cx.update_editor(|editor, window, cx| {
9923 editor.handle_input("{", window, cx);
9924 editor.handle_input("{", window, cx);
9925 editor.handle_input("{", window, cx);
9926 });
9927 cx.assert_editor_state(
9928 &"
9929 🏀{{{ˇ}}}
9930 ε{{{ˇ}}}
9931 ❤️{{{ˇ}}}
9932 "
9933 .unindent(),
9934 );
9935
9936 // insert a different closing bracket
9937 cx.update_editor(|editor, window, cx| {
9938 editor.handle_input(")", window, cx);
9939 });
9940 cx.assert_editor_state(
9941 &"
9942 🏀{{{)ˇ}}}
9943 ε{{{)ˇ}}}
9944 ❤️{{{)ˇ}}}
9945 "
9946 .unindent(),
9947 );
9948
9949 // skip over the auto-closed brackets when typing a closing bracket
9950 cx.update_editor(|editor, window, cx| {
9951 editor.move_right(&MoveRight, window, cx);
9952 editor.handle_input("}", window, cx);
9953 editor.handle_input("}", window, cx);
9954 editor.handle_input("}", window, cx);
9955 });
9956 cx.assert_editor_state(
9957 &"
9958 🏀{{{)}}}}ˇ
9959 ε{{{)}}}}ˇ
9960 ❤️{{{)}}}}ˇ
9961 "
9962 .unindent(),
9963 );
9964
9965 // autoclose multi-character pairs
9966 cx.set_state(
9967 &"
9968 ˇ
9969 ˇ
9970 "
9971 .unindent(),
9972 );
9973 cx.update_editor(|editor, window, cx| {
9974 editor.handle_input("/", window, cx);
9975 editor.handle_input("*", window, cx);
9976 });
9977 cx.assert_editor_state(
9978 &"
9979 /*ˇ */
9980 /*ˇ */
9981 "
9982 .unindent(),
9983 );
9984
9985 // one cursor autocloses a multi-character pair, one cursor
9986 // does not autoclose.
9987 cx.set_state(
9988 &"
9989 /ˇ
9990 ˇ
9991 "
9992 .unindent(),
9993 );
9994 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
9995 cx.assert_editor_state(
9996 &"
9997 /*ˇ */
9998 *ˇ
9999 "
10000 .unindent(),
10001 );
10002
10003 // Don't autoclose if the next character isn't whitespace and isn't
10004 // listed in the language's "autoclose_before" section.
10005 cx.set_state("ˇa b");
10006 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10007 cx.assert_editor_state("{ˇa b");
10008
10009 // Don't autoclose if `close` is false for the bracket pair
10010 cx.set_state("ˇ");
10011 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10012 cx.assert_editor_state("[ˇ");
10013
10014 // Surround with brackets if text is selected
10015 cx.set_state("«aˇ» b");
10016 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10017 cx.assert_editor_state("{«aˇ»} b");
10018
10019 // Autoclose when not immediately after a word character
10020 cx.set_state("a ˇ");
10021 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10022 cx.assert_editor_state("a \"ˇ\"");
10023
10024 // Autoclose pair where the start and end characters are the same
10025 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10026 cx.assert_editor_state("a \"\"ˇ");
10027
10028 // Don't autoclose when immediately after a word character
10029 cx.set_state("aˇ");
10030 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10031 cx.assert_editor_state("a\"ˇ");
10032
10033 // Do autoclose when after a non-word character
10034 cx.set_state("{ˇ");
10035 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10036 cx.assert_editor_state("{\"ˇ\"");
10037
10038 // Non identical pairs autoclose regardless of preceding character
10039 cx.set_state("aˇ");
10040 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10041 cx.assert_editor_state("a{ˇ}");
10042
10043 // Don't autoclose pair if autoclose is disabled
10044 cx.set_state("ˇ");
10045 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10046 cx.assert_editor_state("<ˇ");
10047
10048 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10049 cx.set_state("«aˇ» b");
10050 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10051 cx.assert_editor_state("<«aˇ»> b");
10052}
10053
10054#[gpui::test]
10055async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10056 init_test(cx, |settings| {
10057 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10058 });
10059
10060 let mut cx = EditorTestContext::new(cx).await;
10061
10062 let language = Arc::new(Language::new(
10063 LanguageConfig {
10064 brackets: BracketPairConfig {
10065 pairs: vec![
10066 BracketPair {
10067 start: "{".to_string(),
10068 end: "}".to_string(),
10069 close: true,
10070 surround: true,
10071 newline: true,
10072 },
10073 BracketPair {
10074 start: "(".to_string(),
10075 end: ")".to_string(),
10076 close: true,
10077 surround: true,
10078 newline: true,
10079 },
10080 BracketPair {
10081 start: "[".to_string(),
10082 end: "]".to_string(),
10083 close: false,
10084 surround: false,
10085 newline: true,
10086 },
10087 ],
10088 ..Default::default()
10089 },
10090 autoclose_before: "})]".to_string(),
10091 ..Default::default()
10092 },
10093 Some(tree_sitter_rust::LANGUAGE.into()),
10094 ));
10095
10096 cx.language_registry().add(language.clone());
10097 cx.update_buffer(|buffer, cx| {
10098 buffer.set_language(Some(language), cx);
10099 });
10100
10101 cx.set_state(
10102 &"
10103 ˇ
10104 ˇ
10105 ˇ
10106 "
10107 .unindent(),
10108 );
10109
10110 // ensure only matching closing brackets are skipped over
10111 cx.update_editor(|editor, window, cx| {
10112 editor.handle_input("}", window, cx);
10113 editor.move_left(&MoveLeft, window, cx);
10114 editor.handle_input(")", window, cx);
10115 editor.move_left(&MoveLeft, window, cx);
10116 });
10117 cx.assert_editor_state(
10118 &"
10119 ˇ)}
10120 ˇ)}
10121 ˇ)}
10122 "
10123 .unindent(),
10124 );
10125
10126 // skip-over closing brackets at multiple cursors
10127 cx.update_editor(|editor, window, cx| {
10128 editor.handle_input(")", window, cx);
10129 editor.handle_input("}", window, cx);
10130 });
10131 cx.assert_editor_state(
10132 &"
10133 )}ˇ
10134 )}ˇ
10135 )}ˇ
10136 "
10137 .unindent(),
10138 );
10139
10140 // ignore non-close brackets
10141 cx.update_editor(|editor, window, cx| {
10142 editor.handle_input("]", window, cx);
10143 editor.move_left(&MoveLeft, window, cx);
10144 editor.handle_input("]", window, cx);
10145 });
10146 cx.assert_editor_state(
10147 &"
10148 )}]ˇ]
10149 )}]ˇ]
10150 )}]ˇ]
10151 "
10152 .unindent(),
10153 );
10154}
10155
10156#[gpui::test]
10157async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10158 init_test(cx, |_| {});
10159
10160 let mut cx = EditorTestContext::new(cx).await;
10161
10162 let html_language = Arc::new(
10163 Language::new(
10164 LanguageConfig {
10165 name: "HTML".into(),
10166 brackets: BracketPairConfig {
10167 pairs: vec![
10168 BracketPair {
10169 start: "<".into(),
10170 end: ">".into(),
10171 close: true,
10172 ..Default::default()
10173 },
10174 BracketPair {
10175 start: "{".into(),
10176 end: "}".into(),
10177 close: true,
10178 ..Default::default()
10179 },
10180 BracketPair {
10181 start: "(".into(),
10182 end: ")".into(),
10183 close: true,
10184 ..Default::default()
10185 },
10186 ],
10187 ..Default::default()
10188 },
10189 autoclose_before: "})]>".into(),
10190 ..Default::default()
10191 },
10192 Some(tree_sitter_html::LANGUAGE.into()),
10193 )
10194 .with_injection_query(
10195 r#"
10196 (script_element
10197 (raw_text) @injection.content
10198 (#set! injection.language "javascript"))
10199 "#,
10200 )
10201 .unwrap(),
10202 );
10203
10204 let javascript_language = Arc::new(Language::new(
10205 LanguageConfig {
10206 name: "JavaScript".into(),
10207 brackets: BracketPairConfig {
10208 pairs: vec![
10209 BracketPair {
10210 start: "/*".into(),
10211 end: " */".into(),
10212 close: true,
10213 ..Default::default()
10214 },
10215 BracketPair {
10216 start: "{".into(),
10217 end: "}".into(),
10218 close: true,
10219 ..Default::default()
10220 },
10221 BracketPair {
10222 start: "(".into(),
10223 end: ")".into(),
10224 close: true,
10225 ..Default::default()
10226 },
10227 ],
10228 ..Default::default()
10229 },
10230 autoclose_before: "})]>".into(),
10231 ..Default::default()
10232 },
10233 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10234 ));
10235
10236 cx.language_registry().add(html_language.clone());
10237 cx.language_registry().add(javascript_language);
10238 cx.executor().run_until_parked();
10239
10240 cx.update_buffer(|buffer, cx| {
10241 buffer.set_language(Some(html_language), cx);
10242 });
10243
10244 cx.set_state(
10245 &r#"
10246 <body>ˇ
10247 <script>
10248 var x = 1;ˇ
10249 </script>
10250 </body>ˇ
10251 "#
10252 .unindent(),
10253 );
10254
10255 // Precondition: different languages are active at different locations.
10256 cx.update_editor(|editor, window, cx| {
10257 let snapshot = editor.snapshot(window, cx);
10258 let cursors = editor.selections.ranges::<usize>(cx);
10259 let languages = cursors
10260 .iter()
10261 .map(|c| snapshot.language_at(c.start).unwrap().name())
10262 .collect::<Vec<_>>();
10263 assert_eq!(
10264 languages,
10265 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10266 );
10267 });
10268
10269 // Angle brackets autoclose in HTML, but not JavaScript.
10270 cx.update_editor(|editor, window, cx| {
10271 editor.handle_input("<", window, cx);
10272 editor.handle_input("a", window, cx);
10273 });
10274 cx.assert_editor_state(
10275 &r#"
10276 <body><aˇ>
10277 <script>
10278 var x = 1;<aˇ
10279 </script>
10280 </body><aˇ>
10281 "#
10282 .unindent(),
10283 );
10284
10285 // Curly braces and parens autoclose in both HTML and JavaScript.
10286 cx.update_editor(|editor, window, cx| {
10287 editor.handle_input(" b=", window, cx);
10288 editor.handle_input("{", window, cx);
10289 editor.handle_input("c", window, cx);
10290 editor.handle_input("(", window, cx);
10291 });
10292 cx.assert_editor_state(
10293 &r#"
10294 <body><a b={c(ˇ)}>
10295 <script>
10296 var x = 1;<a b={c(ˇ)}
10297 </script>
10298 </body><a b={c(ˇ)}>
10299 "#
10300 .unindent(),
10301 );
10302
10303 // Brackets that were already autoclosed are skipped.
10304 cx.update_editor(|editor, window, cx| {
10305 editor.handle_input(")", window, cx);
10306 editor.handle_input("d", window, cx);
10307 editor.handle_input("}", window, cx);
10308 });
10309 cx.assert_editor_state(
10310 &r#"
10311 <body><a b={c()d}ˇ>
10312 <script>
10313 var x = 1;<a b={c()d}ˇ
10314 </script>
10315 </body><a b={c()d}ˇ>
10316 "#
10317 .unindent(),
10318 );
10319 cx.update_editor(|editor, window, cx| {
10320 editor.handle_input(">", window, cx);
10321 });
10322 cx.assert_editor_state(
10323 &r#"
10324 <body><a b={c()d}>ˇ
10325 <script>
10326 var x = 1;<a b={c()d}>ˇ
10327 </script>
10328 </body><a b={c()d}>ˇ
10329 "#
10330 .unindent(),
10331 );
10332
10333 // Reset
10334 cx.set_state(
10335 &r#"
10336 <body>ˇ
10337 <script>
10338 var x = 1;ˇ
10339 </script>
10340 </body>ˇ
10341 "#
10342 .unindent(),
10343 );
10344
10345 cx.update_editor(|editor, window, cx| {
10346 editor.handle_input("<", window, cx);
10347 });
10348 cx.assert_editor_state(
10349 &r#"
10350 <body><ˇ>
10351 <script>
10352 var x = 1;<ˇ
10353 </script>
10354 </body><ˇ>
10355 "#
10356 .unindent(),
10357 );
10358
10359 // When backspacing, the closing angle brackets are removed.
10360 cx.update_editor(|editor, window, cx| {
10361 editor.backspace(&Backspace, window, cx);
10362 });
10363 cx.assert_editor_state(
10364 &r#"
10365 <body>ˇ
10366 <script>
10367 var x = 1;ˇ
10368 </script>
10369 </body>ˇ
10370 "#
10371 .unindent(),
10372 );
10373
10374 // Block comments autoclose in JavaScript, but not HTML.
10375 cx.update_editor(|editor, window, cx| {
10376 editor.handle_input("/", window, cx);
10377 editor.handle_input("*", window, cx);
10378 });
10379 cx.assert_editor_state(
10380 &r#"
10381 <body>/*ˇ
10382 <script>
10383 var x = 1;/*ˇ */
10384 </script>
10385 </body>/*ˇ
10386 "#
10387 .unindent(),
10388 );
10389}
10390
10391#[gpui::test]
10392async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10393 init_test(cx, |_| {});
10394
10395 let mut cx = EditorTestContext::new(cx).await;
10396
10397 let rust_language = Arc::new(
10398 Language::new(
10399 LanguageConfig {
10400 name: "Rust".into(),
10401 brackets: serde_json::from_value(json!([
10402 { "start": "{", "end": "}", "close": true, "newline": true },
10403 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10404 ]))
10405 .unwrap(),
10406 autoclose_before: "})]>".into(),
10407 ..Default::default()
10408 },
10409 Some(tree_sitter_rust::LANGUAGE.into()),
10410 )
10411 .with_override_query("(string_literal) @string")
10412 .unwrap(),
10413 );
10414
10415 cx.language_registry().add(rust_language.clone());
10416 cx.update_buffer(|buffer, cx| {
10417 buffer.set_language(Some(rust_language), cx);
10418 });
10419
10420 cx.set_state(
10421 &r#"
10422 let x = ˇ
10423 "#
10424 .unindent(),
10425 );
10426
10427 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10428 cx.update_editor(|editor, window, cx| {
10429 editor.handle_input("\"", window, cx);
10430 });
10431 cx.assert_editor_state(
10432 &r#"
10433 let x = "ˇ"
10434 "#
10435 .unindent(),
10436 );
10437
10438 // Inserting another quotation mark. The cursor moves across the existing
10439 // automatically-inserted quotation mark.
10440 cx.update_editor(|editor, window, cx| {
10441 editor.handle_input("\"", window, cx);
10442 });
10443 cx.assert_editor_state(
10444 &r#"
10445 let x = ""ˇ
10446 "#
10447 .unindent(),
10448 );
10449
10450 // Reset
10451 cx.set_state(
10452 &r#"
10453 let x = ˇ
10454 "#
10455 .unindent(),
10456 );
10457
10458 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10459 cx.update_editor(|editor, window, cx| {
10460 editor.handle_input("\"", window, cx);
10461 editor.handle_input(" ", window, cx);
10462 editor.move_left(&Default::default(), window, cx);
10463 editor.handle_input("\\", window, cx);
10464 editor.handle_input("\"", window, cx);
10465 });
10466 cx.assert_editor_state(
10467 &r#"
10468 let x = "\"ˇ "
10469 "#
10470 .unindent(),
10471 );
10472
10473 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10474 // mark. Nothing is inserted.
10475 cx.update_editor(|editor, window, cx| {
10476 editor.move_right(&Default::default(), window, cx);
10477 editor.handle_input("\"", window, cx);
10478 });
10479 cx.assert_editor_state(
10480 &r#"
10481 let x = "\" "ˇ
10482 "#
10483 .unindent(),
10484 );
10485}
10486
10487#[gpui::test]
10488async fn test_surround_with_pair(cx: &mut TestAppContext) {
10489 init_test(cx, |_| {});
10490
10491 let language = Arc::new(Language::new(
10492 LanguageConfig {
10493 brackets: BracketPairConfig {
10494 pairs: vec![
10495 BracketPair {
10496 start: "{".to_string(),
10497 end: "}".to_string(),
10498 close: true,
10499 surround: true,
10500 newline: true,
10501 },
10502 BracketPair {
10503 start: "/* ".to_string(),
10504 end: "*/".to_string(),
10505 close: true,
10506 surround: true,
10507 ..Default::default()
10508 },
10509 ],
10510 ..Default::default()
10511 },
10512 ..Default::default()
10513 },
10514 Some(tree_sitter_rust::LANGUAGE.into()),
10515 ));
10516
10517 let text = r#"
10518 a
10519 b
10520 c
10521 "#
10522 .unindent();
10523
10524 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10525 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10526 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10527 editor
10528 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10529 .await;
10530
10531 editor.update_in(cx, |editor, window, cx| {
10532 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10533 s.select_display_ranges([
10534 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10535 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10536 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10537 ])
10538 });
10539
10540 editor.handle_input("{", window, cx);
10541 editor.handle_input("{", window, cx);
10542 editor.handle_input("{", window, cx);
10543 assert_eq!(
10544 editor.text(cx),
10545 "
10546 {{{a}}}
10547 {{{b}}}
10548 {{{c}}}
10549 "
10550 .unindent()
10551 );
10552 assert_eq!(
10553 editor.selections.display_ranges(cx),
10554 [
10555 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10556 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10557 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10558 ]
10559 );
10560
10561 editor.undo(&Undo, window, cx);
10562 editor.undo(&Undo, window, cx);
10563 editor.undo(&Undo, window, cx);
10564 assert_eq!(
10565 editor.text(cx),
10566 "
10567 a
10568 b
10569 c
10570 "
10571 .unindent()
10572 );
10573 assert_eq!(
10574 editor.selections.display_ranges(cx),
10575 [
10576 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10577 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10578 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10579 ]
10580 );
10581
10582 // Ensure inserting the first character of a multi-byte bracket pair
10583 // doesn't surround the selections with the bracket.
10584 editor.handle_input("/", window, cx);
10585 assert_eq!(
10586 editor.text(cx),
10587 "
10588 /
10589 /
10590 /
10591 "
10592 .unindent()
10593 );
10594 assert_eq!(
10595 editor.selections.display_ranges(cx),
10596 [
10597 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10598 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10599 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10600 ]
10601 );
10602
10603 editor.undo(&Undo, window, cx);
10604 assert_eq!(
10605 editor.text(cx),
10606 "
10607 a
10608 b
10609 c
10610 "
10611 .unindent()
10612 );
10613 assert_eq!(
10614 editor.selections.display_ranges(cx),
10615 [
10616 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10617 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10618 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10619 ]
10620 );
10621
10622 // Ensure inserting the last character of a multi-byte bracket pair
10623 // doesn't surround the selections with the bracket.
10624 editor.handle_input("*", window, cx);
10625 assert_eq!(
10626 editor.text(cx),
10627 "
10628 *
10629 *
10630 *
10631 "
10632 .unindent()
10633 );
10634 assert_eq!(
10635 editor.selections.display_ranges(cx),
10636 [
10637 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10638 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10639 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10640 ]
10641 );
10642 });
10643}
10644
10645#[gpui::test]
10646async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10647 init_test(cx, |_| {});
10648
10649 let language = Arc::new(Language::new(
10650 LanguageConfig {
10651 brackets: BracketPairConfig {
10652 pairs: vec![BracketPair {
10653 start: "{".to_string(),
10654 end: "}".to_string(),
10655 close: true,
10656 surround: true,
10657 newline: true,
10658 }],
10659 ..Default::default()
10660 },
10661 autoclose_before: "}".to_string(),
10662 ..Default::default()
10663 },
10664 Some(tree_sitter_rust::LANGUAGE.into()),
10665 ));
10666
10667 let text = r#"
10668 a
10669 b
10670 c
10671 "#
10672 .unindent();
10673
10674 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10675 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10676 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10677 editor
10678 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10679 .await;
10680
10681 editor.update_in(cx, |editor, window, cx| {
10682 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10683 s.select_ranges([
10684 Point::new(0, 1)..Point::new(0, 1),
10685 Point::new(1, 1)..Point::new(1, 1),
10686 Point::new(2, 1)..Point::new(2, 1),
10687 ])
10688 });
10689
10690 editor.handle_input("{", window, cx);
10691 editor.handle_input("{", window, cx);
10692 editor.handle_input("_", window, cx);
10693 assert_eq!(
10694 editor.text(cx),
10695 "
10696 a{{_}}
10697 b{{_}}
10698 c{{_}}
10699 "
10700 .unindent()
10701 );
10702 assert_eq!(
10703 editor.selections.ranges::<Point>(cx),
10704 [
10705 Point::new(0, 4)..Point::new(0, 4),
10706 Point::new(1, 4)..Point::new(1, 4),
10707 Point::new(2, 4)..Point::new(2, 4)
10708 ]
10709 );
10710
10711 editor.backspace(&Default::default(), window, cx);
10712 editor.backspace(&Default::default(), window, cx);
10713 assert_eq!(
10714 editor.text(cx),
10715 "
10716 a{}
10717 b{}
10718 c{}
10719 "
10720 .unindent()
10721 );
10722 assert_eq!(
10723 editor.selections.ranges::<Point>(cx),
10724 [
10725 Point::new(0, 2)..Point::new(0, 2),
10726 Point::new(1, 2)..Point::new(1, 2),
10727 Point::new(2, 2)..Point::new(2, 2)
10728 ]
10729 );
10730
10731 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10732 assert_eq!(
10733 editor.text(cx),
10734 "
10735 a
10736 b
10737 c
10738 "
10739 .unindent()
10740 );
10741 assert_eq!(
10742 editor.selections.ranges::<Point>(cx),
10743 [
10744 Point::new(0, 1)..Point::new(0, 1),
10745 Point::new(1, 1)..Point::new(1, 1),
10746 Point::new(2, 1)..Point::new(2, 1)
10747 ]
10748 );
10749 });
10750}
10751
10752#[gpui::test]
10753async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10754 init_test(cx, |settings| {
10755 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10756 });
10757
10758 let mut cx = EditorTestContext::new(cx).await;
10759
10760 let language = Arc::new(Language::new(
10761 LanguageConfig {
10762 brackets: BracketPairConfig {
10763 pairs: vec![
10764 BracketPair {
10765 start: "{".to_string(),
10766 end: "}".to_string(),
10767 close: true,
10768 surround: true,
10769 newline: true,
10770 },
10771 BracketPair {
10772 start: "(".to_string(),
10773 end: ")".to_string(),
10774 close: true,
10775 surround: true,
10776 newline: true,
10777 },
10778 BracketPair {
10779 start: "[".to_string(),
10780 end: "]".to_string(),
10781 close: false,
10782 surround: true,
10783 newline: true,
10784 },
10785 ],
10786 ..Default::default()
10787 },
10788 autoclose_before: "})]".to_string(),
10789 ..Default::default()
10790 },
10791 Some(tree_sitter_rust::LANGUAGE.into()),
10792 ));
10793
10794 cx.language_registry().add(language.clone());
10795 cx.update_buffer(|buffer, cx| {
10796 buffer.set_language(Some(language), cx);
10797 });
10798
10799 cx.set_state(
10800 &"
10801 {(ˇ)}
10802 [[ˇ]]
10803 {(ˇ)}
10804 "
10805 .unindent(),
10806 );
10807
10808 cx.update_editor(|editor, window, cx| {
10809 editor.backspace(&Default::default(), window, cx);
10810 editor.backspace(&Default::default(), window, cx);
10811 });
10812
10813 cx.assert_editor_state(
10814 &"
10815 ˇ
10816 ˇ]]
10817 ˇ
10818 "
10819 .unindent(),
10820 );
10821
10822 cx.update_editor(|editor, window, cx| {
10823 editor.handle_input("{", window, cx);
10824 editor.handle_input("{", window, cx);
10825 editor.move_right(&MoveRight, window, cx);
10826 editor.move_right(&MoveRight, window, cx);
10827 editor.move_left(&MoveLeft, window, cx);
10828 editor.move_left(&MoveLeft, window, cx);
10829 editor.backspace(&Default::default(), window, cx);
10830 });
10831
10832 cx.assert_editor_state(
10833 &"
10834 {ˇ}
10835 {ˇ}]]
10836 {ˇ}
10837 "
10838 .unindent(),
10839 );
10840
10841 cx.update_editor(|editor, window, cx| {
10842 editor.backspace(&Default::default(), window, cx);
10843 });
10844
10845 cx.assert_editor_state(
10846 &"
10847 ˇ
10848 ˇ]]
10849 ˇ
10850 "
10851 .unindent(),
10852 );
10853}
10854
10855#[gpui::test]
10856async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10857 init_test(cx, |_| {});
10858
10859 let language = Arc::new(Language::new(
10860 LanguageConfig::default(),
10861 Some(tree_sitter_rust::LANGUAGE.into()),
10862 ));
10863
10864 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10865 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10866 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10867 editor
10868 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10869 .await;
10870
10871 editor.update_in(cx, |editor, window, cx| {
10872 editor.set_auto_replace_emoji_shortcode(true);
10873
10874 editor.handle_input("Hello ", window, cx);
10875 editor.handle_input(":wave", window, cx);
10876 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10877
10878 editor.handle_input(":", window, cx);
10879 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10880
10881 editor.handle_input(" :smile", window, cx);
10882 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10883
10884 editor.handle_input(":", window, cx);
10885 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10886
10887 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10888 editor.handle_input(":wave", window, cx);
10889 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10890
10891 editor.handle_input(":", window, cx);
10892 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10893
10894 editor.handle_input(":1", window, cx);
10895 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10896
10897 editor.handle_input(":", window, cx);
10898 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10899
10900 // Ensure shortcode does not get replaced when it is part of a word
10901 editor.handle_input(" Test:wave", window, cx);
10902 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
10903
10904 editor.handle_input(":", window, cx);
10905 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
10906
10907 editor.set_auto_replace_emoji_shortcode(false);
10908
10909 // Ensure shortcode does not get replaced when auto replace is off
10910 editor.handle_input(" :wave", window, cx);
10911 assert_eq!(
10912 editor.text(cx),
10913 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
10914 );
10915
10916 editor.handle_input(":", window, cx);
10917 assert_eq!(
10918 editor.text(cx),
10919 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
10920 );
10921 });
10922}
10923
10924#[gpui::test]
10925async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
10926 init_test(cx, |_| {});
10927
10928 let (text, insertion_ranges) = marked_text_ranges(
10929 indoc! {"
10930 ˇ
10931 "},
10932 false,
10933 );
10934
10935 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
10936 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10937
10938 _ = editor.update_in(cx, |editor, window, cx| {
10939 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
10940
10941 editor
10942 .insert_snippet(&insertion_ranges, snippet, window, cx)
10943 .unwrap();
10944
10945 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
10946 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
10947 assert_eq!(editor.text(cx), expected_text);
10948 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
10949 }
10950
10951 assert(
10952 editor,
10953 cx,
10954 indoc! {"
10955 type «» =•
10956 "},
10957 );
10958
10959 assert!(editor.context_menu_visible(), "There should be a matches");
10960 });
10961}
10962
10963#[gpui::test]
10964async fn test_snippets(cx: &mut TestAppContext) {
10965 init_test(cx, |_| {});
10966
10967 let mut cx = EditorTestContext::new(cx).await;
10968
10969 cx.set_state(indoc! {"
10970 a.ˇ b
10971 a.ˇ b
10972 a.ˇ b
10973 "});
10974
10975 cx.update_editor(|editor, window, cx| {
10976 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
10977 let insertion_ranges = editor
10978 .selections
10979 .all(cx)
10980 .iter()
10981 .map(|s| s.range())
10982 .collect::<Vec<_>>();
10983 editor
10984 .insert_snippet(&insertion_ranges, snippet, window, cx)
10985 .unwrap();
10986 });
10987
10988 cx.assert_editor_state(indoc! {"
10989 a.f(«oneˇ», two, «threeˇ») b
10990 a.f(«oneˇ», two, «threeˇ») b
10991 a.f(«oneˇ», two, «threeˇ») b
10992 "});
10993
10994 // Can't move earlier than the first tab stop
10995 cx.update_editor(|editor, window, cx| {
10996 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10997 });
10998 cx.assert_editor_state(indoc! {"
10999 a.f(«oneˇ», two, «threeˇ») b
11000 a.f(«oneˇ», two, «threeˇ») b
11001 a.f(«oneˇ», two, «threeˇ») b
11002 "});
11003
11004 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11005 cx.assert_editor_state(indoc! {"
11006 a.f(one, «twoˇ», three) b
11007 a.f(one, «twoˇ», three) b
11008 a.f(one, «twoˇ», three) b
11009 "});
11010
11011 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11012 cx.assert_editor_state(indoc! {"
11013 a.f(«oneˇ», two, «threeˇ») b
11014 a.f(«oneˇ», two, «threeˇ») b
11015 a.f(«oneˇ», two, «threeˇ») b
11016 "});
11017
11018 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11019 cx.assert_editor_state(indoc! {"
11020 a.f(one, «twoˇ», three) b
11021 a.f(one, «twoˇ», three) b
11022 a.f(one, «twoˇ», three) b
11023 "});
11024 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11025 cx.assert_editor_state(indoc! {"
11026 a.f(one, two, three)ˇ b
11027 a.f(one, two, three)ˇ b
11028 a.f(one, two, three)ˇ b
11029 "});
11030
11031 // As soon as the last tab stop is reached, snippet state is gone
11032 cx.update_editor(|editor, window, cx| {
11033 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11034 });
11035 cx.assert_editor_state(indoc! {"
11036 a.f(one, two, three)ˇ b
11037 a.f(one, two, three)ˇ b
11038 a.f(one, two, three)ˇ b
11039 "});
11040}
11041
11042#[gpui::test]
11043async fn test_snippet_indentation(cx: &mut TestAppContext) {
11044 init_test(cx, |_| {});
11045
11046 let mut cx = EditorTestContext::new(cx).await;
11047
11048 cx.update_editor(|editor, window, cx| {
11049 let snippet = Snippet::parse(indoc! {"
11050 /*
11051 * Multiline comment with leading indentation
11052 *
11053 * $1
11054 */
11055 $0"})
11056 .unwrap();
11057 let insertion_ranges = editor
11058 .selections
11059 .all(cx)
11060 .iter()
11061 .map(|s| s.range())
11062 .collect::<Vec<_>>();
11063 editor
11064 .insert_snippet(&insertion_ranges, snippet, window, cx)
11065 .unwrap();
11066 });
11067
11068 cx.assert_editor_state(indoc! {"
11069 /*
11070 * Multiline comment with leading indentation
11071 *
11072 * ˇ
11073 */
11074 "});
11075
11076 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11077 cx.assert_editor_state(indoc! {"
11078 /*
11079 * Multiline comment with leading indentation
11080 *
11081 *•
11082 */
11083 ˇ"});
11084}
11085
11086#[gpui::test]
11087async fn test_document_format_during_save(cx: &mut TestAppContext) {
11088 init_test(cx, |_| {});
11089
11090 let fs = FakeFs::new(cx.executor());
11091 fs.insert_file(path!("/file.rs"), Default::default()).await;
11092
11093 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11094
11095 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11096 language_registry.add(rust_lang());
11097 let mut fake_servers = language_registry.register_fake_lsp(
11098 "Rust",
11099 FakeLspAdapter {
11100 capabilities: lsp::ServerCapabilities {
11101 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11102 ..Default::default()
11103 },
11104 ..Default::default()
11105 },
11106 );
11107
11108 let buffer = project
11109 .update(cx, |project, cx| {
11110 project.open_local_buffer(path!("/file.rs"), cx)
11111 })
11112 .await
11113 .unwrap();
11114
11115 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11116 let (editor, cx) = cx.add_window_view(|window, cx| {
11117 build_editor_with_project(project.clone(), buffer, window, cx)
11118 });
11119 editor.update_in(cx, |editor, window, cx| {
11120 editor.set_text("one\ntwo\nthree\n", window, cx)
11121 });
11122 assert!(cx.read(|cx| editor.is_dirty(cx)));
11123
11124 cx.executor().start_waiting();
11125 let fake_server = fake_servers.next().await.unwrap();
11126
11127 {
11128 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11129 move |params, _| async move {
11130 assert_eq!(
11131 params.text_document.uri,
11132 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11133 );
11134 assert_eq!(params.options.tab_size, 4);
11135 Ok(Some(vec![lsp::TextEdit::new(
11136 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11137 ", ".to_string(),
11138 )]))
11139 },
11140 );
11141 let save = editor
11142 .update_in(cx, |editor, window, cx| {
11143 editor.save(
11144 SaveOptions {
11145 format: true,
11146 autosave: false,
11147 },
11148 project.clone(),
11149 window,
11150 cx,
11151 )
11152 })
11153 .unwrap();
11154 cx.executor().start_waiting();
11155 save.await;
11156
11157 assert_eq!(
11158 editor.update(cx, |editor, cx| editor.text(cx)),
11159 "one, two\nthree\n"
11160 );
11161 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11162 }
11163
11164 {
11165 editor.update_in(cx, |editor, window, cx| {
11166 editor.set_text("one\ntwo\nthree\n", window, cx)
11167 });
11168 assert!(cx.read(|cx| editor.is_dirty(cx)));
11169
11170 // Ensure we can still save even if formatting hangs.
11171 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11172 move |params, _| async move {
11173 assert_eq!(
11174 params.text_document.uri,
11175 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11176 );
11177 futures::future::pending::<()>().await;
11178 unreachable!()
11179 },
11180 );
11181 let save = editor
11182 .update_in(cx, |editor, window, cx| {
11183 editor.save(
11184 SaveOptions {
11185 format: true,
11186 autosave: false,
11187 },
11188 project.clone(),
11189 window,
11190 cx,
11191 )
11192 })
11193 .unwrap();
11194 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11195 cx.executor().start_waiting();
11196 save.await;
11197 assert_eq!(
11198 editor.update(cx, |editor, cx| editor.text(cx)),
11199 "one\ntwo\nthree\n"
11200 );
11201 }
11202
11203 // Set rust language override and assert overridden tabsize is sent to language server
11204 update_test_language_settings(cx, |settings| {
11205 settings.languages.0.insert(
11206 "Rust".into(),
11207 LanguageSettingsContent {
11208 tab_size: NonZeroU32::new(8),
11209 ..Default::default()
11210 },
11211 );
11212 });
11213
11214 {
11215 editor.update_in(cx, |editor, window, cx| {
11216 editor.set_text("somehting_new\n", window, cx)
11217 });
11218 assert!(cx.read(|cx| editor.is_dirty(cx)));
11219 let _formatting_request_signal = fake_server
11220 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11221 assert_eq!(
11222 params.text_document.uri,
11223 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11224 );
11225 assert_eq!(params.options.tab_size, 8);
11226 Ok(Some(vec![]))
11227 });
11228 let save = editor
11229 .update_in(cx, |editor, window, cx| {
11230 editor.save(
11231 SaveOptions {
11232 format: true,
11233 autosave: false,
11234 },
11235 project.clone(),
11236 window,
11237 cx,
11238 )
11239 })
11240 .unwrap();
11241 cx.executor().start_waiting();
11242 save.await;
11243 }
11244}
11245
11246#[gpui::test]
11247async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11248 init_test(cx, |settings| {
11249 settings.defaults.ensure_final_newline_on_save = Some(false);
11250 });
11251
11252 let fs = FakeFs::new(cx.executor());
11253 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11254
11255 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11256
11257 let buffer = project
11258 .update(cx, |project, cx| {
11259 project.open_local_buffer(path!("/file.txt"), cx)
11260 })
11261 .await
11262 .unwrap();
11263
11264 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11265 let (editor, cx) = cx.add_window_view(|window, cx| {
11266 build_editor_with_project(project.clone(), buffer, window, cx)
11267 });
11268 editor.update_in(cx, |editor, window, cx| {
11269 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11270 s.select_ranges([0..0])
11271 });
11272 });
11273 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11274
11275 editor.update_in(cx, |editor, window, cx| {
11276 editor.handle_input("\n", window, cx)
11277 });
11278 cx.run_until_parked();
11279 save(&editor, &project, cx).await;
11280 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11281
11282 editor.update_in(cx, |editor, window, cx| {
11283 editor.undo(&Default::default(), window, cx);
11284 });
11285 save(&editor, &project, cx).await;
11286 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11287
11288 editor.update_in(cx, |editor, window, cx| {
11289 editor.redo(&Default::default(), window, cx);
11290 });
11291 cx.run_until_parked();
11292 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11293
11294 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11295 let save = editor
11296 .update_in(cx, |editor, window, cx| {
11297 editor.save(
11298 SaveOptions {
11299 format: true,
11300 autosave: false,
11301 },
11302 project.clone(),
11303 window,
11304 cx,
11305 )
11306 })
11307 .unwrap();
11308 cx.executor().start_waiting();
11309 save.await;
11310 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11311 }
11312}
11313
11314#[gpui::test]
11315async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11316 init_test(cx, |_| {});
11317
11318 let cols = 4;
11319 let rows = 10;
11320 let sample_text_1 = sample_text(rows, cols, 'a');
11321 assert_eq!(
11322 sample_text_1,
11323 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11324 );
11325 let sample_text_2 = sample_text(rows, cols, 'l');
11326 assert_eq!(
11327 sample_text_2,
11328 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11329 );
11330 let sample_text_3 = sample_text(rows, cols, 'v');
11331 assert_eq!(
11332 sample_text_3,
11333 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11334 );
11335
11336 let fs = FakeFs::new(cx.executor());
11337 fs.insert_tree(
11338 path!("/a"),
11339 json!({
11340 "main.rs": sample_text_1,
11341 "other.rs": sample_text_2,
11342 "lib.rs": sample_text_3,
11343 }),
11344 )
11345 .await;
11346
11347 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11348 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11349 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11350
11351 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11352 language_registry.add(rust_lang());
11353 let mut fake_servers = language_registry.register_fake_lsp(
11354 "Rust",
11355 FakeLspAdapter {
11356 capabilities: lsp::ServerCapabilities {
11357 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11358 ..Default::default()
11359 },
11360 ..Default::default()
11361 },
11362 );
11363
11364 let worktree = project.update(cx, |project, cx| {
11365 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11366 assert_eq!(worktrees.len(), 1);
11367 worktrees.pop().unwrap()
11368 });
11369 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11370
11371 let buffer_1 = project
11372 .update(cx, |project, cx| {
11373 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11374 })
11375 .await
11376 .unwrap();
11377 let buffer_2 = project
11378 .update(cx, |project, cx| {
11379 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11380 })
11381 .await
11382 .unwrap();
11383 let buffer_3 = project
11384 .update(cx, |project, cx| {
11385 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11386 })
11387 .await
11388 .unwrap();
11389
11390 let multi_buffer = cx.new(|cx| {
11391 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11392 multi_buffer.push_excerpts(
11393 buffer_1.clone(),
11394 [
11395 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11396 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11397 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11398 ],
11399 cx,
11400 );
11401 multi_buffer.push_excerpts(
11402 buffer_2.clone(),
11403 [
11404 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11405 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11406 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11407 ],
11408 cx,
11409 );
11410 multi_buffer.push_excerpts(
11411 buffer_3.clone(),
11412 [
11413 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11414 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11415 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11416 ],
11417 cx,
11418 );
11419 multi_buffer
11420 });
11421 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11422 Editor::new(
11423 EditorMode::full(),
11424 multi_buffer,
11425 Some(project.clone()),
11426 window,
11427 cx,
11428 )
11429 });
11430
11431 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11432 editor.change_selections(
11433 SelectionEffects::scroll(Autoscroll::Next),
11434 window,
11435 cx,
11436 |s| s.select_ranges(Some(1..2)),
11437 );
11438 editor.insert("|one|two|three|", window, cx);
11439 });
11440 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11441 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11442 editor.change_selections(
11443 SelectionEffects::scroll(Autoscroll::Next),
11444 window,
11445 cx,
11446 |s| s.select_ranges(Some(60..70)),
11447 );
11448 editor.insert("|four|five|six|", window, cx);
11449 });
11450 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11451
11452 // First two buffers should be edited, but not the third one.
11453 assert_eq!(
11454 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11455 "a|one|two|three|aa\nbbbb\ncccc\n\nffff\ngggg\n\njjjj\nllll\nmmmm\nnnnn|four|five|six|\nr\n\nuuuu\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}",
11456 );
11457 buffer_1.update(cx, |buffer, _| {
11458 assert!(buffer.is_dirty());
11459 assert_eq!(
11460 buffer.text(),
11461 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11462 )
11463 });
11464 buffer_2.update(cx, |buffer, _| {
11465 assert!(buffer.is_dirty());
11466 assert_eq!(
11467 buffer.text(),
11468 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11469 )
11470 });
11471 buffer_3.update(cx, |buffer, _| {
11472 assert!(!buffer.is_dirty());
11473 assert_eq!(buffer.text(), sample_text_3,)
11474 });
11475 cx.executor().run_until_parked();
11476
11477 cx.executor().start_waiting();
11478 let save = multi_buffer_editor
11479 .update_in(cx, |editor, window, cx| {
11480 editor.save(
11481 SaveOptions {
11482 format: true,
11483 autosave: false,
11484 },
11485 project.clone(),
11486 window,
11487 cx,
11488 )
11489 })
11490 .unwrap();
11491
11492 let fake_server = fake_servers.next().await.unwrap();
11493 fake_server
11494 .server
11495 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11496 Ok(Some(vec![lsp::TextEdit::new(
11497 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11498 format!("[{} formatted]", params.text_document.uri),
11499 )]))
11500 })
11501 .detach();
11502 save.await;
11503
11504 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11505 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11506 assert_eq!(
11507 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11508 uri!(
11509 "a|o[file:///a/main.rs formatted]bbbb\ncccc\n\nffff\ngggg\n\njjjj\n\nlll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|\nr\n\nuuuu\n\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}"
11510 ),
11511 );
11512 buffer_1.update(cx, |buffer, _| {
11513 assert!(!buffer.is_dirty());
11514 assert_eq!(
11515 buffer.text(),
11516 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11517 )
11518 });
11519 buffer_2.update(cx, |buffer, _| {
11520 assert!(!buffer.is_dirty());
11521 assert_eq!(
11522 buffer.text(),
11523 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11524 )
11525 });
11526 buffer_3.update(cx, |buffer, _| {
11527 assert!(!buffer.is_dirty());
11528 assert_eq!(buffer.text(), sample_text_3,)
11529 });
11530}
11531
11532#[gpui::test]
11533async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11534 init_test(cx, |_| {});
11535
11536 let fs = FakeFs::new(cx.executor());
11537 fs.insert_tree(
11538 path!("/dir"),
11539 json!({
11540 "file1.rs": "fn main() { println!(\"hello\"); }",
11541 "file2.rs": "fn test() { println!(\"test\"); }",
11542 "file3.rs": "fn other() { println!(\"other\"); }\n",
11543 }),
11544 )
11545 .await;
11546
11547 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11548 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11549 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11550
11551 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11552 language_registry.add(rust_lang());
11553
11554 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11555 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11556
11557 // Open three buffers
11558 let buffer_1 = project
11559 .update(cx, |project, cx| {
11560 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11561 })
11562 .await
11563 .unwrap();
11564 let buffer_2 = project
11565 .update(cx, |project, cx| {
11566 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11567 })
11568 .await
11569 .unwrap();
11570 let buffer_3 = project
11571 .update(cx, |project, cx| {
11572 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11573 })
11574 .await
11575 .unwrap();
11576
11577 // Create a multi-buffer with all three buffers
11578 let multi_buffer = cx.new(|cx| {
11579 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11580 multi_buffer.push_excerpts(
11581 buffer_1.clone(),
11582 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11583 cx,
11584 );
11585 multi_buffer.push_excerpts(
11586 buffer_2.clone(),
11587 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11588 cx,
11589 );
11590 multi_buffer.push_excerpts(
11591 buffer_3.clone(),
11592 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11593 cx,
11594 );
11595 multi_buffer
11596 });
11597
11598 let editor = cx.new_window_entity(|window, cx| {
11599 Editor::new(
11600 EditorMode::full(),
11601 multi_buffer,
11602 Some(project.clone()),
11603 window,
11604 cx,
11605 )
11606 });
11607
11608 // Edit only the first buffer
11609 editor.update_in(cx, |editor, window, cx| {
11610 editor.change_selections(
11611 SelectionEffects::scroll(Autoscroll::Next),
11612 window,
11613 cx,
11614 |s| s.select_ranges(Some(10..10)),
11615 );
11616 editor.insert("// edited", window, cx);
11617 });
11618
11619 // Verify that only buffer 1 is dirty
11620 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11621 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11622 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11623
11624 // Get write counts after file creation (files were created with initial content)
11625 // We expect each file to have been written once during creation
11626 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11627 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11628 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11629
11630 // Perform autosave
11631 let save_task = editor.update_in(cx, |editor, window, cx| {
11632 editor.save(
11633 SaveOptions {
11634 format: true,
11635 autosave: true,
11636 },
11637 project.clone(),
11638 window,
11639 cx,
11640 )
11641 });
11642 save_task.await.unwrap();
11643
11644 // Only the dirty buffer should have been saved
11645 assert_eq!(
11646 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11647 1,
11648 "Buffer 1 was dirty, so it should have been written once during autosave"
11649 );
11650 assert_eq!(
11651 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11652 0,
11653 "Buffer 2 was clean, so it should not have been written during autosave"
11654 );
11655 assert_eq!(
11656 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11657 0,
11658 "Buffer 3 was clean, so it should not have been written during autosave"
11659 );
11660
11661 // Verify buffer states after autosave
11662 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11663 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11664 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11665
11666 // Now perform a manual save (format = true)
11667 let save_task = editor.update_in(cx, |editor, window, cx| {
11668 editor.save(
11669 SaveOptions {
11670 format: true,
11671 autosave: false,
11672 },
11673 project.clone(),
11674 window,
11675 cx,
11676 )
11677 });
11678 save_task.await.unwrap();
11679
11680 // During manual save, clean buffers don't get written to disk
11681 // They just get did_save called for language server notifications
11682 assert_eq!(
11683 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11684 1,
11685 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11686 );
11687 assert_eq!(
11688 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11689 0,
11690 "Buffer 2 should not have been written at all"
11691 );
11692 assert_eq!(
11693 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11694 0,
11695 "Buffer 3 should not have been written at all"
11696 );
11697}
11698
11699async fn setup_range_format_test(
11700 cx: &mut TestAppContext,
11701) -> (
11702 Entity<Project>,
11703 Entity<Editor>,
11704 &mut gpui::VisualTestContext,
11705 lsp::FakeLanguageServer,
11706) {
11707 init_test(cx, |_| {});
11708
11709 let fs = FakeFs::new(cx.executor());
11710 fs.insert_file(path!("/file.rs"), Default::default()).await;
11711
11712 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11713
11714 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11715 language_registry.add(rust_lang());
11716 let mut fake_servers = language_registry.register_fake_lsp(
11717 "Rust",
11718 FakeLspAdapter {
11719 capabilities: lsp::ServerCapabilities {
11720 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11721 ..lsp::ServerCapabilities::default()
11722 },
11723 ..FakeLspAdapter::default()
11724 },
11725 );
11726
11727 let buffer = project
11728 .update(cx, |project, cx| {
11729 project.open_local_buffer(path!("/file.rs"), cx)
11730 })
11731 .await
11732 .unwrap();
11733
11734 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11735 let (editor, cx) = cx.add_window_view(|window, cx| {
11736 build_editor_with_project(project.clone(), buffer, window, cx)
11737 });
11738
11739 cx.executor().start_waiting();
11740 let fake_server = fake_servers.next().await.unwrap();
11741
11742 (project, editor, cx, fake_server)
11743}
11744
11745#[gpui::test]
11746async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11747 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11748
11749 editor.update_in(cx, |editor, window, cx| {
11750 editor.set_text("one\ntwo\nthree\n", window, cx)
11751 });
11752 assert!(cx.read(|cx| editor.is_dirty(cx)));
11753
11754 let save = editor
11755 .update_in(cx, |editor, window, cx| {
11756 editor.save(
11757 SaveOptions {
11758 format: true,
11759 autosave: false,
11760 },
11761 project.clone(),
11762 window,
11763 cx,
11764 )
11765 })
11766 .unwrap();
11767 fake_server
11768 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11769 assert_eq!(
11770 params.text_document.uri,
11771 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11772 );
11773 assert_eq!(params.options.tab_size, 4);
11774 Ok(Some(vec![lsp::TextEdit::new(
11775 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11776 ", ".to_string(),
11777 )]))
11778 })
11779 .next()
11780 .await;
11781 cx.executor().start_waiting();
11782 save.await;
11783 assert_eq!(
11784 editor.update(cx, |editor, cx| editor.text(cx)),
11785 "one, two\nthree\n"
11786 );
11787 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11788}
11789
11790#[gpui::test]
11791async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11792 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11793
11794 editor.update_in(cx, |editor, window, cx| {
11795 editor.set_text("one\ntwo\nthree\n", window, cx)
11796 });
11797 assert!(cx.read(|cx| editor.is_dirty(cx)));
11798
11799 // Test that save still works when formatting hangs
11800 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11801 move |params, _| async move {
11802 assert_eq!(
11803 params.text_document.uri,
11804 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11805 );
11806 futures::future::pending::<()>().await;
11807 unreachable!()
11808 },
11809 );
11810 let save = editor
11811 .update_in(cx, |editor, window, cx| {
11812 editor.save(
11813 SaveOptions {
11814 format: true,
11815 autosave: false,
11816 },
11817 project.clone(),
11818 window,
11819 cx,
11820 )
11821 })
11822 .unwrap();
11823 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11824 cx.executor().start_waiting();
11825 save.await;
11826 assert_eq!(
11827 editor.update(cx, |editor, cx| editor.text(cx)),
11828 "one\ntwo\nthree\n"
11829 );
11830 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11831}
11832
11833#[gpui::test]
11834async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11835 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11836
11837 // Buffer starts clean, no formatting should be requested
11838 let save = editor
11839 .update_in(cx, |editor, window, cx| {
11840 editor.save(
11841 SaveOptions {
11842 format: false,
11843 autosave: false,
11844 },
11845 project.clone(),
11846 window,
11847 cx,
11848 )
11849 })
11850 .unwrap();
11851 let _pending_format_request = fake_server
11852 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11853 panic!("Should not be invoked");
11854 })
11855 .next();
11856 cx.executor().start_waiting();
11857 save.await;
11858 cx.run_until_parked();
11859}
11860
11861#[gpui::test]
11862async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11863 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11864
11865 // Set Rust language override and assert overridden tabsize is sent to language server
11866 update_test_language_settings(cx, |settings| {
11867 settings.languages.0.insert(
11868 "Rust".into(),
11869 LanguageSettingsContent {
11870 tab_size: NonZeroU32::new(8),
11871 ..Default::default()
11872 },
11873 );
11874 });
11875
11876 editor.update_in(cx, |editor, window, cx| {
11877 editor.set_text("something_new\n", window, cx)
11878 });
11879 assert!(cx.read(|cx| editor.is_dirty(cx)));
11880 let save = editor
11881 .update_in(cx, |editor, window, cx| {
11882 editor.save(
11883 SaveOptions {
11884 format: true,
11885 autosave: false,
11886 },
11887 project.clone(),
11888 window,
11889 cx,
11890 )
11891 })
11892 .unwrap();
11893 fake_server
11894 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11895 assert_eq!(
11896 params.text_document.uri,
11897 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11898 );
11899 assert_eq!(params.options.tab_size, 8);
11900 Ok(Some(Vec::new()))
11901 })
11902 .next()
11903 .await;
11904 save.await;
11905}
11906
11907#[gpui::test]
11908async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
11909 init_test(cx, |settings| {
11910 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
11911 settings::LanguageServerFormatterSpecifier::Current,
11912 )))
11913 });
11914
11915 let fs = FakeFs::new(cx.executor());
11916 fs.insert_file(path!("/file.rs"), Default::default()).await;
11917
11918 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11919
11920 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11921 language_registry.add(Arc::new(Language::new(
11922 LanguageConfig {
11923 name: "Rust".into(),
11924 matcher: LanguageMatcher {
11925 path_suffixes: vec!["rs".to_string()],
11926 ..Default::default()
11927 },
11928 ..LanguageConfig::default()
11929 },
11930 Some(tree_sitter_rust::LANGUAGE.into()),
11931 )));
11932 update_test_language_settings(cx, |settings| {
11933 // Enable Prettier formatting for the same buffer, and ensure
11934 // LSP is called instead of Prettier.
11935 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
11936 });
11937 let mut fake_servers = language_registry.register_fake_lsp(
11938 "Rust",
11939 FakeLspAdapter {
11940 capabilities: lsp::ServerCapabilities {
11941 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11942 ..Default::default()
11943 },
11944 ..Default::default()
11945 },
11946 );
11947
11948 let buffer = project
11949 .update(cx, |project, cx| {
11950 project.open_local_buffer(path!("/file.rs"), cx)
11951 })
11952 .await
11953 .unwrap();
11954
11955 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11956 let (editor, cx) = cx.add_window_view(|window, cx| {
11957 build_editor_with_project(project.clone(), buffer, window, cx)
11958 });
11959 editor.update_in(cx, |editor, window, cx| {
11960 editor.set_text("one\ntwo\nthree\n", window, cx)
11961 });
11962
11963 cx.executor().start_waiting();
11964 let fake_server = fake_servers.next().await.unwrap();
11965
11966 let format = editor
11967 .update_in(cx, |editor, window, cx| {
11968 editor.perform_format(
11969 project.clone(),
11970 FormatTrigger::Manual,
11971 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11972 window,
11973 cx,
11974 )
11975 })
11976 .unwrap();
11977 fake_server
11978 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11979 assert_eq!(
11980 params.text_document.uri,
11981 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11982 );
11983 assert_eq!(params.options.tab_size, 4);
11984 Ok(Some(vec![lsp::TextEdit::new(
11985 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11986 ", ".to_string(),
11987 )]))
11988 })
11989 .next()
11990 .await;
11991 cx.executor().start_waiting();
11992 format.await;
11993 assert_eq!(
11994 editor.update(cx, |editor, cx| editor.text(cx)),
11995 "one, two\nthree\n"
11996 );
11997
11998 editor.update_in(cx, |editor, window, cx| {
11999 editor.set_text("one\ntwo\nthree\n", window, cx)
12000 });
12001 // Ensure we don't lock if formatting hangs.
12002 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12003 move |params, _| async move {
12004 assert_eq!(
12005 params.text_document.uri,
12006 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12007 );
12008 futures::future::pending::<()>().await;
12009 unreachable!()
12010 },
12011 );
12012 let format = editor
12013 .update_in(cx, |editor, window, cx| {
12014 editor.perform_format(
12015 project,
12016 FormatTrigger::Manual,
12017 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12018 window,
12019 cx,
12020 )
12021 })
12022 .unwrap();
12023 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12024 cx.executor().start_waiting();
12025 format.await;
12026 assert_eq!(
12027 editor.update(cx, |editor, cx| editor.text(cx)),
12028 "one\ntwo\nthree\n"
12029 );
12030}
12031
12032#[gpui::test]
12033async fn test_multiple_formatters(cx: &mut TestAppContext) {
12034 init_test(cx, |settings| {
12035 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12036 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12037 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12038 Formatter::CodeAction("code-action-1".into()),
12039 Formatter::CodeAction("code-action-2".into()),
12040 ]))
12041 });
12042
12043 let fs = FakeFs::new(cx.executor());
12044 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12045 .await;
12046
12047 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12048 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12049 language_registry.add(rust_lang());
12050
12051 let mut fake_servers = language_registry.register_fake_lsp(
12052 "Rust",
12053 FakeLspAdapter {
12054 capabilities: lsp::ServerCapabilities {
12055 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12056 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12057 commands: vec!["the-command-for-code-action-1".into()],
12058 ..Default::default()
12059 }),
12060 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12061 ..Default::default()
12062 },
12063 ..Default::default()
12064 },
12065 );
12066
12067 let buffer = project
12068 .update(cx, |project, cx| {
12069 project.open_local_buffer(path!("/file.rs"), cx)
12070 })
12071 .await
12072 .unwrap();
12073
12074 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12075 let (editor, cx) = cx.add_window_view(|window, cx| {
12076 build_editor_with_project(project.clone(), buffer, window, cx)
12077 });
12078
12079 cx.executor().start_waiting();
12080
12081 let fake_server = fake_servers.next().await.unwrap();
12082 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12083 move |_params, _| async move {
12084 Ok(Some(vec![lsp::TextEdit::new(
12085 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12086 "applied-formatting\n".to_string(),
12087 )]))
12088 },
12089 );
12090 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12091 move |params, _| async move {
12092 let requested_code_actions = params.context.only.expect("Expected code action request");
12093 assert_eq!(requested_code_actions.len(), 1);
12094
12095 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12096 let code_action = match requested_code_actions[0].as_str() {
12097 "code-action-1" => lsp::CodeAction {
12098 kind: Some("code-action-1".into()),
12099 edit: Some(lsp::WorkspaceEdit::new(
12100 [(
12101 uri,
12102 vec![lsp::TextEdit::new(
12103 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12104 "applied-code-action-1-edit\n".to_string(),
12105 )],
12106 )]
12107 .into_iter()
12108 .collect(),
12109 )),
12110 command: Some(lsp::Command {
12111 command: "the-command-for-code-action-1".into(),
12112 ..Default::default()
12113 }),
12114 ..Default::default()
12115 },
12116 "code-action-2" => lsp::CodeAction {
12117 kind: Some("code-action-2".into()),
12118 edit: Some(lsp::WorkspaceEdit::new(
12119 [(
12120 uri,
12121 vec![lsp::TextEdit::new(
12122 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12123 "applied-code-action-2-edit\n".to_string(),
12124 )],
12125 )]
12126 .into_iter()
12127 .collect(),
12128 )),
12129 ..Default::default()
12130 },
12131 req => panic!("Unexpected code action request: {:?}", req),
12132 };
12133 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12134 code_action,
12135 )]))
12136 },
12137 );
12138
12139 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12140 move |params, _| async move { Ok(params) }
12141 });
12142
12143 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12144 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12145 let fake = fake_server.clone();
12146 let lock = command_lock.clone();
12147 move |params, _| {
12148 assert_eq!(params.command, "the-command-for-code-action-1");
12149 let fake = fake.clone();
12150 let lock = lock.clone();
12151 async move {
12152 lock.lock().await;
12153 fake.server
12154 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12155 label: None,
12156 edit: lsp::WorkspaceEdit {
12157 changes: Some(
12158 [(
12159 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12160 vec![lsp::TextEdit {
12161 range: lsp::Range::new(
12162 lsp::Position::new(0, 0),
12163 lsp::Position::new(0, 0),
12164 ),
12165 new_text: "applied-code-action-1-command\n".into(),
12166 }],
12167 )]
12168 .into_iter()
12169 .collect(),
12170 ),
12171 ..Default::default()
12172 },
12173 })
12174 .await
12175 .into_response()
12176 .unwrap();
12177 Ok(Some(json!(null)))
12178 }
12179 }
12180 });
12181
12182 cx.executor().start_waiting();
12183 editor
12184 .update_in(cx, |editor, window, cx| {
12185 editor.perform_format(
12186 project.clone(),
12187 FormatTrigger::Manual,
12188 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12189 window,
12190 cx,
12191 )
12192 })
12193 .unwrap()
12194 .await;
12195 editor.update(cx, |editor, cx| {
12196 assert_eq!(
12197 editor.text(cx),
12198 r#"
12199 applied-code-action-2-edit
12200 applied-code-action-1-command
12201 applied-code-action-1-edit
12202 applied-formatting
12203 one
12204 two
12205 three
12206 "#
12207 .unindent()
12208 );
12209 });
12210
12211 editor.update_in(cx, |editor, window, cx| {
12212 editor.undo(&Default::default(), window, cx);
12213 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12214 });
12215
12216 // Perform a manual edit while waiting for an LSP command
12217 // that's being run as part of a formatting code action.
12218 let lock_guard = command_lock.lock().await;
12219 let format = editor
12220 .update_in(cx, |editor, window, cx| {
12221 editor.perform_format(
12222 project.clone(),
12223 FormatTrigger::Manual,
12224 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12225 window,
12226 cx,
12227 )
12228 })
12229 .unwrap();
12230 cx.run_until_parked();
12231 editor.update(cx, |editor, cx| {
12232 assert_eq!(
12233 editor.text(cx),
12234 r#"
12235 applied-code-action-1-edit
12236 applied-formatting
12237 one
12238 two
12239 three
12240 "#
12241 .unindent()
12242 );
12243
12244 editor.buffer.update(cx, |buffer, cx| {
12245 let ix = buffer.len(cx);
12246 buffer.edit([(ix..ix, "edited\n")], None, cx);
12247 });
12248 });
12249
12250 // Allow the LSP command to proceed. Because the buffer was edited,
12251 // the second code action will not be run.
12252 drop(lock_guard);
12253 format.await;
12254 editor.update_in(cx, |editor, window, cx| {
12255 assert_eq!(
12256 editor.text(cx),
12257 r#"
12258 applied-code-action-1-command
12259 applied-code-action-1-edit
12260 applied-formatting
12261 one
12262 two
12263 three
12264 edited
12265 "#
12266 .unindent()
12267 );
12268
12269 // The manual edit is undone first, because it is the last thing the user did
12270 // (even though the command completed afterwards).
12271 editor.undo(&Default::default(), window, cx);
12272 assert_eq!(
12273 editor.text(cx),
12274 r#"
12275 applied-code-action-1-command
12276 applied-code-action-1-edit
12277 applied-formatting
12278 one
12279 two
12280 three
12281 "#
12282 .unindent()
12283 );
12284
12285 // All the formatting (including the command, which completed after the manual edit)
12286 // is undone together.
12287 editor.undo(&Default::default(), window, cx);
12288 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12289 });
12290}
12291
12292#[gpui::test]
12293async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12294 init_test(cx, |settings| {
12295 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12296 settings::LanguageServerFormatterSpecifier::Current,
12297 )]))
12298 });
12299
12300 let fs = FakeFs::new(cx.executor());
12301 fs.insert_file(path!("/file.ts"), Default::default()).await;
12302
12303 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12304
12305 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12306 language_registry.add(Arc::new(Language::new(
12307 LanguageConfig {
12308 name: "TypeScript".into(),
12309 matcher: LanguageMatcher {
12310 path_suffixes: vec!["ts".to_string()],
12311 ..Default::default()
12312 },
12313 ..LanguageConfig::default()
12314 },
12315 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12316 )));
12317 update_test_language_settings(cx, |settings| {
12318 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12319 });
12320 let mut fake_servers = language_registry.register_fake_lsp(
12321 "TypeScript",
12322 FakeLspAdapter {
12323 capabilities: lsp::ServerCapabilities {
12324 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12325 ..Default::default()
12326 },
12327 ..Default::default()
12328 },
12329 );
12330
12331 let buffer = project
12332 .update(cx, |project, cx| {
12333 project.open_local_buffer(path!("/file.ts"), cx)
12334 })
12335 .await
12336 .unwrap();
12337
12338 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12339 let (editor, cx) = cx.add_window_view(|window, cx| {
12340 build_editor_with_project(project.clone(), buffer, window, cx)
12341 });
12342 editor.update_in(cx, |editor, window, cx| {
12343 editor.set_text(
12344 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12345 window,
12346 cx,
12347 )
12348 });
12349
12350 cx.executor().start_waiting();
12351 let fake_server = fake_servers.next().await.unwrap();
12352
12353 let format = editor
12354 .update_in(cx, |editor, window, cx| {
12355 editor.perform_code_action_kind(
12356 project.clone(),
12357 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12358 window,
12359 cx,
12360 )
12361 })
12362 .unwrap();
12363 fake_server
12364 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12365 assert_eq!(
12366 params.text_document.uri,
12367 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12368 );
12369 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12370 lsp::CodeAction {
12371 title: "Organize Imports".to_string(),
12372 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12373 edit: Some(lsp::WorkspaceEdit {
12374 changes: Some(
12375 [(
12376 params.text_document.uri.clone(),
12377 vec![lsp::TextEdit::new(
12378 lsp::Range::new(
12379 lsp::Position::new(1, 0),
12380 lsp::Position::new(2, 0),
12381 ),
12382 "".to_string(),
12383 )],
12384 )]
12385 .into_iter()
12386 .collect(),
12387 ),
12388 ..Default::default()
12389 }),
12390 ..Default::default()
12391 },
12392 )]))
12393 })
12394 .next()
12395 .await;
12396 cx.executor().start_waiting();
12397 format.await;
12398 assert_eq!(
12399 editor.update(cx, |editor, cx| editor.text(cx)),
12400 "import { a } from 'module';\n\nconst x = a;\n"
12401 );
12402
12403 editor.update_in(cx, |editor, window, cx| {
12404 editor.set_text(
12405 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12406 window,
12407 cx,
12408 )
12409 });
12410 // Ensure we don't lock if code action hangs.
12411 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12412 move |params, _| async move {
12413 assert_eq!(
12414 params.text_document.uri,
12415 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12416 );
12417 futures::future::pending::<()>().await;
12418 unreachable!()
12419 },
12420 );
12421 let format = editor
12422 .update_in(cx, |editor, window, cx| {
12423 editor.perform_code_action_kind(
12424 project,
12425 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12426 window,
12427 cx,
12428 )
12429 })
12430 .unwrap();
12431 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12432 cx.executor().start_waiting();
12433 format.await;
12434 assert_eq!(
12435 editor.update(cx, |editor, cx| editor.text(cx)),
12436 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12437 );
12438}
12439
12440#[gpui::test]
12441async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12442 init_test(cx, |_| {});
12443
12444 let mut cx = EditorLspTestContext::new_rust(
12445 lsp::ServerCapabilities {
12446 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12447 ..Default::default()
12448 },
12449 cx,
12450 )
12451 .await;
12452
12453 cx.set_state(indoc! {"
12454 one.twoˇ
12455 "});
12456
12457 // The format request takes a long time. When it completes, it inserts
12458 // a newline and an indent before the `.`
12459 cx.lsp
12460 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12461 let executor = cx.background_executor().clone();
12462 async move {
12463 executor.timer(Duration::from_millis(100)).await;
12464 Ok(Some(vec![lsp::TextEdit {
12465 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12466 new_text: "\n ".into(),
12467 }]))
12468 }
12469 });
12470
12471 // Submit a format request.
12472 let format_1 = cx
12473 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12474 .unwrap();
12475 cx.executor().run_until_parked();
12476
12477 // Submit a second format request.
12478 let format_2 = cx
12479 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12480 .unwrap();
12481 cx.executor().run_until_parked();
12482
12483 // Wait for both format requests to complete
12484 cx.executor().advance_clock(Duration::from_millis(200));
12485 cx.executor().start_waiting();
12486 format_1.await.unwrap();
12487 cx.executor().start_waiting();
12488 format_2.await.unwrap();
12489
12490 // The formatting edits only happens once.
12491 cx.assert_editor_state(indoc! {"
12492 one
12493 .twoˇ
12494 "});
12495}
12496
12497#[gpui::test]
12498async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12499 init_test(cx, |settings| {
12500 settings.defaults.formatter = Some(FormatterList::default())
12501 });
12502
12503 let mut cx = EditorLspTestContext::new_rust(
12504 lsp::ServerCapabilities {
12505 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12506 ..Default::default()
12507 },
12508 cx,
12509 )
12510 .await;
12511
12512 // Set up a buffer white some trailing whitespace and no trailing newline.
12513 cx.set_state(
12514 &[
12515 "one ", //
12516 "twoˇ", //
12517 "three ", //
12518 "four", //
12519 ]
12520 .join("\n"),
12521 );
12522
12523 // Record which buffer changes have been sent to the language server
12524 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12525 cx.lsp
12526 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12527 let buffer_changes = buffer_changes.clone();
12528 move |params, _| {
12529 buffer_changes.lock().extend(
12530 params
12531 .content_changes
12532 .into_iter()
12533 .map(|e| (e.range.unwrap(), e.text)),
12534 );
12535 }
12536 });
12537
12538 // Handle formatting requests to the language server.
12539 cx.lsp
12540 .set_request_handler::<lsp::request::Formatting, _, _>({
12541 let buffer_changes = buffer_changes.clone();
12542 move |_, _| {
12543 let buffer_changes = buffer_changes.clone();
12544 // Insert blank lines between each line of the buffer.
12545 async move {
12546 // When formatting is requested, trailing whitespace has already been stripped,
12547 // and the trailing newline has already been added.
12548 assert_eq!(
12549 &buffer_changes.lock()[1..],
12550 &[
12551 (
12552 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12553 "".into()
12554 ),
12555 (
12556 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12557 "".into()
12558 ),
12559 (
12560 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12561 "\n".into()
12562 ),
12563 ]
12564 );
12565
12566 Ok(Some(vec![
12567 lsp::TextEdit {
12568 range: lsp::Range::new(
12569 lsp::Position::new(1, 0),
12570 lsp::Position::new(1, 0),
12571 ),
12572 new_text: "\n".into(),
12573 },
12574 lsp::TextEdit {
12575 range: lsp::Range::new(
12576 lsp::Position::new(2, 0),
12577 lsp::Position::new(2, 0),
12578 ),
12579 new_text: "\n".into(),
12580 },
12581 ]))
12582 }
12583 }
12584 });
12585
12586 // Submit a format request.
12587 let format = cx
12588 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12589 .unwrap();
12590
12591 cx.run_until_parked();
12592 // After formatting the buffer, the trailing whitespace is stripped,
12593 // a newline is appended, and the edits provided by the language server
12594 // have been applied.
12595 format.await.unwrap();
12596
12597 cx.assert_editor_state(
12598 &[
12599 "one", //
12600 "", //
12601 "twoˇ", //
12602 "", //
12603 "three", //
12604 "four", //
12605 "", //
12606 ]
12607 .join("\n"),
12608 );
12609
12610 // Undoing the formatting undoes the trailing whitespace removal, the
12611 // trailing newline, and the LSP edits.
12612 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12613 cx.assert_editor_state(
12614 &[
12615 "one ", //
12616 "twoˇ", //
12617 "three ", //
12618 "four", //
12619 ]
12620 .join("\n"),
12621 );
12622}
12623
12624#[gpui::test]
12625async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12626 cx: &mut TestAppContext,
12627) {
12628 init_test(cx, |_| {});
12629
12630 cx.update(|cx| {
12631 cx.update_global::<SettingsStore, _>(|settings, cx| {
12632 settings.update_user_settings(cx, |settings| {
12633 settings.editor.auto_signature_help = Some(true);
12634 });
12635 });
12636 });
12637
12638 let mut cx = EditorLspTestContext::new_rust(
12639 lsp::ServerCapabilities {
12640 signature_help_provider: Some(lsp::SignatureHelpOptions {
12641 ..Default::default()
12642 }),
12643 ..Default::default()
12644 },
12645 cx,
12646 )
12647 .await;
12648
12649 let language = Language::new(
12650 LanguageConfig {
12651 name: "Rust".into(),
12652 brackets: BracketPairConfig {
12653 pairs: vec![
12654 BracketPair {
12655 start: "{".to_string(),
12656 end: "}".to_string(),
12657 close: true,
12658 surround: true,
12659 newline: true,
12660 },
12661 BracketPair {
12662 start: "(".to_string(),
12663 end: ")".to_string(),
12664 close: true,
12665 surround: true,
12666 newline: true,
12667 },
12668 BracketPair {
12669 start: "/*".to_string(),
12670 end: " */".to_string(),
12671 close: true,
12672 surround: true,
12673 newline: true,
12674 },
12675 BracketPair {
12676 start: "[".to_string(),
12677 end: "]".to_string(),
12678 close: false,
12679 surround: false,
12680 newline: true,
12681 },
12682 BracketPair {
12683 start: "\"".to_string(),
12684 end: "\"".to_string(),
12685 close: true,
12686 surround: true,
12687 newline: false,
12688 },
12689 BracketPair {
12690 start: "<".to_string(),
12691 end: ">".to_string(),
12692 close: false,
12693 surround: true,
12694 newline: true,
12695 },
12696 ],
12697 ..Default::default()
12698 },
12699 autoclose_before: "})]".to_string(),
12700 ..Default::default()
12701 },
12702 Some(tree_sitter_rust::LANGUAGE.into()),
12703 );
12704 let language = Arc::new(language);
12705
12706 cx.language_registry().add(language.clone());
12707 cx.update_buffer(|buffer, cx| {
12708 buffer.set_language(Some(language), cx);
12709 });
12710
12711 cx.set_state(
12712 &r#"
12713 fn main() {
12714 sampleˇ
12715 }
12716 "#
12717 .unindent(),
12718 );
12719
12720 cx.update_editor(|editor, window, cx| {
12721 editor.handle_input("(", window, cx);
12722 });
12723 cx.assert_editor_state(
12724 &"
12725 fn main() {
12726 sample(ˇ)
12727 }
12728 "
12729 .unindent(),
12730 );
12731
12732 let mocked_response = lsp::SignatureHelp {
12733 signatures: vec![lsp::SignatureInformation {
12734 label: "fn sample(param1: u8, param2: u8)".to_string(),
12735 documentation: None,
12736 parameters: Some(vec![
12737 lsp::ParameterInformation {
12738 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12739 documentation: None,
12740 },
12741 lsp::ParameterInformation {
12742 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12743 documentation: None,
12744 },
12745 ]),
12746 active_parameter: None,
12747 }],
12748 active_signature: Some(0),
12749 active_parameter: Some(0),
12750 };
12751 handle_signature_help_request(&mut cx, mocked_response).await;
12752
12753 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12754 .await;
12755
12756 cx.editor(|editor, _, _| {
12757 let signature_help_state = editor.signature_help_state.popover().cloned();
12758 let signature = signature_help_state.unwrap();
12759 assert_eq!(
12760 signature.signatures[signature.current_signature].label,
12761 "fn sample(param1: u8, param2: u8)"
12762 );
12763 });
12764}
12765
12766#[gpui::test]
12767async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12768 init_test(cx, |_| {});
12769
12770 cx.update(|cx| {
12771 cx.update_global::<SettingsStore, _>(|settings, cx| {
12772 settings.update_user_settings(cx, |settings| {
12773 settings.editor.auto_signature_help = Some(false);
12774 settings.editor.show_signature_help_after_edits = Some(false);
12775 });
12776 });
12777 });
12778
12779 let mut cx = EditorLspTestContext::new_rust(
12780 lsp::ServerCapabilities {
12781 signature_help_provider: Some(lsp::SignatureHelpOptions {
12782 ..Default::default()
12783 }),
12784 ..Default::default()
12785 },
12786 cx,
12787 )
12788 .await;
12789
12790 let language = Language::new(
12791 LanguageConfig {
12792 name: "Rust".into(),
12793 brackets: BracketPairConfig {
12794 pairs: vec![
12795 BracketPair {
12796 start: "{".to_string(),
12797 end: "}".to_string(),
12798 close: true,
12799 surround: true,
12800 newline: true,
12801 },
12802 BracketPair {
12803 start: "(".to_string(),
12804 end: ")".to_string(),
12805 close: true,
12806 surround: true,
12807 newline: true,
12808 },
12809 BracketPair {
12810 start: "/*".to_string(),
12811 end: " */".to_string(),
12812 close: true,
12813 surround: true,
12814 newline: true,
12815 },
12816 BracketPair {
12817 start: "[".to_string(),
12818 end: "]".to_string(),
12819 close: false,
12820 surround: false,
12821 newline: true,
12822 },
12823 BracketPair {
12824 start: "\"".to_string(),
12825 end: "\"".to_string(),
12826 close: true,
12827 surround: true,
12828 newline: false,
12829 },
12830 BracketPair {
12831 start: "<".to_string(),
12832 end: ">".to_string(),
12833 close: false,
12834 surround: true,
12835 newline: true,
12836 },
12837 ],
12838 ..Default::default()
12839 },
12840 autoclose_before: "})]".to_string(),
12841 ..Default::default()
12842 },
12843 Some(tree_sitter_rust::LANGUAGE.into()),
12844 );
12845 let language = Arc::new(language);
12846
12847 cx.language_registry().add(language.clone());
12848 cx.update_buffer(|buffer, cx| {
12849 buffer.set_language(Some(language), cx);
12850 });
12851
12852 // Ensure that signature_help is not called when no signature help is enabled.
12853 cx.set_state(
12854 &r#"
12855 fn main() {
12856 sampleˇ
12857 }
12858 "#
12859 .unindent(),
12860 );
12861 cx.update_editor(|editor, window, cx| {
12862 editor.handle_input("(", window, cx);
12863 });
12864 cx.assert_editor_state(
12865 &"
12866 fn main() {
12867 sample(ˇ)
12868 }
12869 "
12870 .unindent(),
12871 );
12872 cx.editor(|editor, _, _| {
12873 assert!(editor.signature_help_state.task().is_none());
12874 });
12875
12876 let mocked_response = lsp::SignatureHelp {
12877 signatures: vec![lsp::SignatureInformation {
12878 label: "fn sample(param1: u8, param2: u8)".to_string(),
12879 documentation: None,
12880 parameters: Some(vec![
12881 lsp::ParameterInformation {
12882 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12883 documentation: None,
12884 },
12885 lsp::ParameterInformation {
12886 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12887 documentation: None,
12888 },
12889 ]),
12890 active_parameter: None,
12891 }],
12892 active_signature: Some(0),
12893 active_parameter: Some(0),
12894 };
12895
12896 // Ensure that signature_help is called when enabled afte edits
12897 cx.update(|_, cx| {
12898 cx.update_global::<SettingsStore, _>(|settings, cx| {
12899 settings.update_user_settings(cx, |settings| {
12900 settings.editor.auto_signature_help = Some(false);
12901 settings.editor.show_signature_help_after_edits = Some(true);
12902 });
12903 });
12904 });
12905 cx.set_state(
12906 &r#"
12907 fn main() {
12908 sampleˇ
12909 }
12910 "#
12911 .unindent(),
12912 );
12913 cx.update_editor(|editor, window, cx| {
12914 editor.handle_input("(", window, cx);
12915 });
12916 cx.assert_editor_state(
12917 &"
12918 fn main() {
12919 sample(ˇ)
12920 }
12921 "
12922 .unindent(),
12923 );
12924 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12925 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12926 .await;
12927 cx.update_editor(|editor, _, _| {
12928 let signature_help_state = editor.signature_help_state.popover().cloned();
12929 assert!(signature_help_state.is_some());
12930 let signature = signature_help_state.unwrap();
12931 assert_eq!(
12932 signature.signatures[signature.current_signature].label,
12933 "fn sample(param1: u8, param2: u8)"
12934 );
12935 editor.signature_help_state = SignatureHelpState::default();
12936 });
12937
12938 // Ensure that signature_help is called when auto signature help override is enabled
12939 cx.update(|_, cx| {
12940 cx.update_global::<SettingsStore, _>(|settings, cx| {
12941 settings.update_user_settings(cx, |settings| {
12942 settings.editor.auto_signature_help = Some(true);
12943 settings.editor.show_signature_help_after_edits = Some(false);
12944 });
12945 });
12946 });
12947 cx.set_state(
12948 &r#"
12949 fn main() {
12950 sampleˇ
12951 }
12952 "#
12953 .unindent(),
12954 );
12955 cx.update_editor(|editor, window, cx| {
12956 editor.handle_input("(", window, cx);
12957 });
12958 cx.assert_editor_state(
12959 &"
12960 fn main() {
12961 sample(ˇ)
12962 }
12963 "
12964 .unindent(),
12965 );
12966 handle_signature_help_request(&mut cx, mocked_response).await;
12967 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12968 .await;
12969 cx.editor(|editor, _, _| {
12970 let signature_help_state = editor.signature_help_state.popover().cloned();
12971 assert!(signature_help_state.is_some());
12972 let signature = signature_help_state.unwrap();
12973 assert_eq!(
12974 signature.signatures[signature.current_signature].label,
12975 "fn sample(param1: u8, param2: u8)"
12976 );
12977 });
12978}
12979
12980#[gpui::test]
12981async fn test_signature_help(cx: &mut TestAppContext) {
12982 init_test(cx, |_| {});
12983 cx.update(|cx| {
12984 cx.update_global::<SettingsStore, _>(|settings, cx| {
12985 settings.update_user_settings(cx, |settings| {
12986 settings.editor.auto_signature_help = Some(true);
12987 });
12988 });
12989 });
12990
12991 let mut cx = EditorLspTestContext::new_rust(
12992 lsp::ServerCapabilities {
12993 signature_help_provider: Some(lsp::SignatureHelpOptions {
12994 ..Default::default()
12995 }),
12996 ..Default::default()
12997 },
12998 cx,
12999 )
13000 .await;
13001
13002 // A test that directly calls `show_signature_help`
13003 cx.update_editor(|editor, window, cx| {
13004 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13005 });
13006
13007 let mocked_response = lsp::SignatureHelp {
13008 signatures: vec![lsp::SignatureInformation {
13009 label: "fn sample(param1: u8, param2: u8)".to_string(),
13010 documentation: None,
13011 parameters: Some(vec![
13012 lsp::ParameterInformation {
13013 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13014 documentation: None,
13015 },
13016 lsp::ParameterInformation {
13017 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13018 documentation: None,
13019 },
13020 ]),
13021 active_parameter: None,
13022 }],
13023 active_signature: Some(0),
13024 active_parameter: Some(0),
13025 };
13026 handle_signature_help_request(&mut cx, mocked_response).await;
13027
13028 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13029 .await;
13030
13031 cx.editor(|editor, _, _| {
13032 let signature_help_state = editor.signature_help_state.popover().cloned();
13033 assert!(signature_help_state.is_some());
13034 let signature = signature_help_state.unwrap();
13035 assert_eq!(
13036 signature.signatures[signature.current_signature].label,
13037 "fn sample(param1: u8, param2: u8)"
13038 );
13039 });
13040
13041 // When exiting outside from inside the brackets, `signature_help` is closed.
13042 cx.set_state(indoc! {"
13043 fn main() {
13044 sample(ˇ);
13045 }
13046
13047 fn sample(param1: u8, param2: u8) {}
13048 "});
13049
13050 cx.update_editor(|editor, window, cx| {
13051 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13052 s.select_ranges([0..0])
13053 });
13054 });
13055
13056 let mocked_response = lsp::SignatureHelp {
13057 signatures: Vec::new(),
13058 active_signature: None,
13059 active_parameter: None,
13060 };
13061 handle_signature_help_request(&mut cx, mocked_response).await;
13062
13063 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13064 .await;
13065
13066 cx.editor(|editor, _, _| {
13067 assert!(!editor.signature_help_state.is_shown());
13068 });
13069
13070 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13071 cx.set_state(indoc! {"
13072 fn main() {
13073 sample(ˇ);
13074 }
13075
13076 fn sample(param1: u8, param2: u8) {}
13077 "});
13078
13079 let mocked_response = lsp::SignatureHelp {
13080 signatures: vec![lsp::SignatureInformation {
13081 label: "fn sample(param1: u8, param2: u8)".to_string(),
13082 documentation: None,
13083 parameters: Some(vec![
13084 lsp::ParameterInformation {
13085 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13086 documentation: None,
13087 },
13088 lsp::ParameterInformation {
13089 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13090 documentation: None,
13091 },
13092 ]),
13093 active_parameter: None,
13094 }],
13095 active_signature: Some(0),
13096 active_parameter: Some(0),
13097 };
13098 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13099 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13100 .await;
13101 cx.editor(|editor, _, _| {
13102 assert!(editor.signature_help_state.is_shown());
13103 });
13104
13105 // Restore the popover with more parameter input
13106 cx.set_state(indoc! {"
13107 fn main() {
13108 sample(param1, param2ˇ);
13109 }
13110
13111 fn sample(param1: u8, param2: u8) {}
13112 "});
13113
13114 let mocked_response = lsp::SignatureHelp {
13115 signatures: vec![lsp::SignatureInformation {
13116 label: "fn sample(param1: u8, param2: u8)".to_string(),
13117 documentation: None,
13118 parameters: Some(vec![
13119 lsp::ParameterInformation {
13120 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13121 documentation: None,
13122 },
13123 lsp::ParameterInformation {
13124 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13125 documentation: None,
13126 },
13127 ]),
13128 active_parameter: None,
13129 }],
13130 active_signature: Some(0),
13131 active_parameter: Some(1),
13132 };
13133 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13134 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13135 .await;
13136
13137 // When selecting a range, the popover is gone.
13138 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13139 cx.update_editor(|editor, window, cx| {
13140 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13141 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13142 })
13143 });
13144 cx.assert_editor_state(indoc! {"
13145 fn main() {
13146 sample(param1, «ˇparam2»);
13147 }
13148
13149 fn sample(param1: u8, param2: u8) {}
13150 "});
13151 cx.editor(|editor, _, _| {
13152 assert!(!editor.signature_help_state.is_shown());
13153 });
13154
13155 // When unselecting again, the popover is back if within the brackets.
13156 cx.update_editor(|editor, window, cx| {
13157 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13158 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13159 })
13160 });
13161 cx.assert_editor_state(indoc! {"
13162 fn main() {
13163 sample(param1, ˇparam2);
13164 }
13165
13166 fn sample(param1: u8, param2: u8) {}
13167 "});
13168 handle_signature_help_request(&mut cx, mocked_response).await;
13169 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13170 .await;
13171 cx.editor(|editor, _, _| {
13172 assert!(editor.signature_help_state.is_shown());
13173 });
13174
13175 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13176 cx.update_editor(|editor, window, cx| {
13177 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13178 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13179 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13180 })
13181 });
13182 cx.assert_editor_state(indoc! {"
13183 fn main() {
13184 sample(param1, ˇparam2);
13185 }
13186
13187 fn sample(param1: u8, param2: u8) {}
13188 "});
13189
13190 let mocked_response = lsp::SignatureHelp {
13191 signatures: vec![lsp::SignatureInformation {
13192 label: "fn sample(param1: u8, param2: u8)".to_string(),
13193 documentation: None,
13194 parameters: Some(vec![
13195 lsp::ParameterInformation {
13196 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13197 documentation: None,
13198 },
13199 lsp::ParameterInformation {
13200 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13201 documentation: None,
13202 },
13203 ]),
13204 active_parameter: None,
13205 }],
13206 active_signature: Some(0),
13207 active_parameter: Some(1),
13208 };
13209 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13210 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13211 .await;
13212 cx.update_editor(|editor, _, cx| {
13213 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13214 });
13215 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13216 .await;
13217 cx.update_editor(|editor, window, cx| {
13218 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13219 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13220 })
13221 });
13222 cx.assert_editor_state(indoc! {"
13223 fn main() {
13224 sample(param1, «ˇparam2»);
13225 }
13226
13227 fn sample(param1: u8, param2: u8) {}
13228 "});
13229 cx.update_editor(|editor, window, cx| {
13230 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13231 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13232 })
13233 });
13234 cx.assert_editor_state(indoc! {"
13235 fn main() {
13236 sample(param1, ˇparam2);
13237 }
13238
13239 fn sample(param1: u8, param2: u8) {}
13240 "});
13241 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13242 .await;
13243}
13244
13245#[gpui::test]
13246async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13247 init_test(cx, |_| {});
13248
13249 let mut cx = EditorLspTestContext::new_rust(
13250 lsp::ServerCapabilities {
13251 signature_help_provider: Some(lsp::SignatureHelpOptions {
13252 ..Default::default()
13253 }),
13254 ..Default::default()
13255 },
13256 cx,
13257 )
13258 .await;
13259
13260 cx.set_state(indoc! {"
13261 fn main() {
13262 overloadedˇ
13263 }
13264 "});
13265
13266 cx.update_editor(|editor, window, cx| {
13267 editor.handle_input("(", window, cx);
13268 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13269 });
13270
13271 // Mock response with 3 signatures
13272 let mocked_response = lsp::SignatureHelp {
13273 signatures: vec![
13274 lsp::SignatureInformation {
13275 label: "fn overloaded(x: i32)".to_string(),
13276 documentation: None,
13277 parameters: Some(vec![lsp::ParameterInformation {
13278 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13279 documentation: None,
13280 }]),
13281 active_parameter: None,
13282 },
13283 lsp::SignatureInformation {
13284 label: "fn overloaded(x: i32, y: i32)".to_string(),
13285 documentation: None,
13286 parameters: Some(vec![
13287 lsp::ParameterInformation {
13288 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13289 documentation: None,
13290 },
13291 lsp::ParameterInformation {
13292 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13293 documentation: None,
13294 },
13295 ]),
13296 active_parameter: None,
13297 },
13298 lsp::SignatureInformation {
13299 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13300 documentation: None,
13301 parameters: Some(vec![
13302 lsp::ParameterInformation {
13303 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13304 documentation: None,
13305 },
13306 lsp::ParameterInformation {
13307 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13308 documentation: None,
13309 },
13310 lsp::ParameterInformation {
13311 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13312 documentation: None,
13313 },
13314 ]),
13315 active_parameter: None,
13316 },
13317 ],
13318 active_signature: Some(1),
13319 active_parameter: Some(0),
13320 };
13321 handle_signature_help_request(&mut cx, mocked_response).await;
13322
13323 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13324 .await;
13325
13326 // Verify we have multiple signatures and the right one is selected
13327 cx.editor(|editor, _, _| {
13328 let popover = editor.signature_help_state.popover().cloned().unwrap();
13329 assert_eq!(popover.signatures.len(), 3);
13330 // active_signature was 1, so that should be the current
13331 assert_eq!(popover.current_signature, 1);
13332 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13333 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13334 assert_eq!(
13335 popover.signatures[2].label,
13336 "fn overloaded(x: i32, y: i32, z: i32)"
13337 );
13338 });
13339
13340 // Test navigation functionality
13341 cx.update_editor(|editor, window, cx| {
13342 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13343 });
13344
13345 cx.editor(|editor, _, _| {
13346 let popover = editor.signature_help_state.popover().cloned().unwrap();
13347 assert_eq!(popover.current_signature, 2);
13348 });
13349
13350 // Test wrap around
13351 cx.update_editor(|editor, window, cx| {
13352 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13353 });
13354
13355 cx.editor(|editor, _, _| {
13356 let popover = editor.signature_help_state.popover().cloned().unwrap();
13357 assert_eq!(popover.current_signature, 0);
13358 });
13359
13360 // Test previous navigation
13361 cx.update_editor(|editor, window, cx| {
13362 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13363 });
13364
13365 cx.editor(|editor, _, _| {
13366 let popover = editor.signature_help_state.popover().cloned().unwrap();
13367 assert_eq!(popover.current_signature, 2);
13368 });
13369}
13370
13371#[gpui::test]
13372async fn test_completion_mode(cx: &mut TestAppContext) {
13373 init_test(cx, |_| {});
13374 let mut cx = EditorLspTestContext::new_rust(
13375 lsp::ServerCapabilities {
13376 completion_provider: Some(lsp::CompletionOptions {
13377 resolve_provider: Some(true),
13378 ..Default::default()
13379 }),
13380 ..Default::default()
13381 },
13382 cx,
13383 )
13384 .await;
13385
13386 struct Run {
13387 run_description: &'static str,
13388 initial_state: String,
13389 buffer_marked_text: String,
13390 completion_label: &'static str,
13391 completion_text: &'static str,
13392 expected_with_insert_mode: String,
13393 expected_with_replace_mode: String,
13394 expected_with_replace_subsequence_mode: String,
13395 expected_with_replace_suffix_mode: String,
13396 }
13397
13398 let runs = [
13399 Run {
13400 run_description: "Start of word matches completion text",
13401 initial_state: "before ediˇ after".into(),
13402 buffer_marked_text: "before <edi|> after".into(),
13403 completion_label: "editor",
13404 completion_text: "editor",
13405 expected_with_insert_mode: "before editorˇ after".into(),
13406 expected_with_replace_mode: "before editorˇ after".into(),
13407 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13408 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13409 },
13410 Run {
13411 run_description: "Accept same text at the middle of the word",
13412 initial_state: "before ediˇtor after".into(),
13413 buffer_marked_text: "before <edi|tor> after".into(),
13414 completion_label: "editor",
13415 completion_text: "editor",
13416 expected_with_insert_mode: "before editorˇtor after".into(),
13417 expected_with_replace_mode: "before editorˇ after".into(),
13418 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13419 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13420 },
13421 Run {
13422 run_description: "End of word matches completion text -- cursor at end",
13423 initial_state: "before torˇ after".into(),
13424 buffer_marked_text: "before <tor|> after".into(),
13425 completion_label: "editor",
13426 completion_text: "editor",
13427 expected_with_insert_mode: "before editorˇ after".into(),
13428 expected_with_replace_mode: "before editorˇ after".into(),
13429 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13430 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13431 },
13432 Run {
13433 run_description: "End of word matches completion text -- cursor at start",
13434 initial_state: "before ˇtor after".into(),
13435 buffer_marked_text: "before <|tor> after".into(),
13436 completion_label: "editor",
13437 completion_text: "editor",
13438 expected_with_insert_mode: "before editorˇtor after".into(),
13439 expected_with_replace_mode: "before editorˇ after".into(),
13440 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13441 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13442 },
13443 Run {
13444 run_description: "Prepend text containing whitespace",
13445 initial_state: "pˇfield: bool".into(),
13446 buffer_marked_text: "<p|field>: bool".into(),
13447 completion_label: "pub ",
13448 completion_text: "pub ",
13449 expected_with_insert_mode: "pub ˇfield: bool".into(),
13450 expected_with_replace_mode: "pub ˇ: bool".into(),
13451 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13452 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13453 },
13454 Run {
13455 run_description: "Add element to start of list",
13456 initial_state: "[element_ˇelement_2]".into(),
13457 buffer_marked_text: "[<element_|element_2>]".into(),
13458 completion_label: "element_1",
13459 completion_text: "element_1",
13460 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13461 expected_with_replace_mode: "[element_1ˇ]".into(),
13462 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13463 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13464 },
13465 Run {
13466 run_description: "Add element to start of list -- first and second elements are equal",
13467 initial_state: "[elˇelement]".into(),
13468 buffer_marked_text: "[<el|element>]".into(),
13469 completion_label: "element",
13470 completion_text: "element",
13471 expected_with_insert_mode: "[elementˇelement]".into(),
13472 expected_with_replace_mode: "[elementˇ]".into(),
13473 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13474 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13475 },
13476 Run {
13477 run_description: "Ends with matching suffix",
13478 initial_state: "SubˇError".into(),
13479 buffer_marked_text: "<Sub|Error>".into(),
13480 completion_label: "SubscriptionError",
13481 completion_text: "SubscriptionError",
13482 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13483 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13484 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13485 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13486 },
13487 Run {
13488 run_description: "Suffix is a subsequence -- contiguous",
13489 initial_state: "SubˇErr".into(),
13490 buffer_marked_text: "<Sub|Err>".into(),
13491 completion_label: "SubscriptionError",
13492 completion_text: "SubscriptionError",
13493 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13494 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13495 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13496 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13497 },
13498 Run {
13499 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13500 initial_state: "Suˇscrirr".into(),
13501 buffer_marked_text: "<Su|scrirr>".into(),
13502 completion_label: "SubscriptionError",
13503 completion_text: "SubscriptionError",
13504 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13505 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13506 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13507 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13508 },
13509 Run {
13510 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13511 initial_state: "foo(indˇix)".into(),
13512 buffer_marked_text: "foo(<ind|ix>)".into(),
13513 completion_label: "node_index",
13514 completion_text: "node_index",
13515 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13516 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13517 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13518 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13519 },
13520 Run {
13521 run_description: "Replace range ends before cursor - should extend to cursor",
13522 initial_state: "before editˇo after".into(),
13523 buffer_marked_text: "before <{ed}>it|o after".into(),
13524 completion_label: "editor",
13525 completion_text: "editor",
13526 expected_with_insert_mode: "before editorˇo after".into(),
13527 expected_with_replace_mode: "before editorˇo after".into(),
13528 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13529 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13530 },
13531 Run {
13532 run_description: "Uses label for suffix matching",
13533 initial_state: "before ediˇtor after".into(),
13534 buffer_marked_text: "before <edi|tor> after".into(),
13535 completion_label: "editor",
13536 completion_text: "editor()",
13537 expected_with_insert_mode: "before editor()ˇtor after".into(),
13538 expected_with_replace_mode: "before editor()ˇ after".into(),
13539 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13540 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13541 },
13542 Run {
13543 run_description: "Case insensitive subsequence and suffix matching",
13544 initial_state: "before EDiˇtoR after".into(),
13545 buffer_marked_text: "before <EDi|toR> after".into(),
13546 completion_label: "editor",
13547 completion_text: "editor",
13548 expected_with_insert_mode: "before editorˇtoR after".into(),
13549 expected_with_replace_mode: "before editorˇ after".into(),
13550 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13551 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13552 },
13553 ];
13554
13555 for run in runs {
13556 let run_variations = [
13557 (LspInsertMode::Insert, run.expected_with_insert_mode),
13558 (LspInsertMode::Replace, run.expected_with_replace_mode),
13559 (
13560 LspInsertMode::ReplaceSubsequence,
13561 run.expected_with_replace_subsequence_mode,
13562 ),
13563 (
13564 LspInsertMode::ReplaceSuffix,
13565 run.expected_with_replace_suffix_mode,
13566 ),
13567 ];
13568
13569 for (lsp_insert_mode, expected_text) in run_variations {
13570 eprintln!(
13571 "run = {:?}, mode = {lsp_insert_mode:.?}",
13572 run.run_description,
13573 );
13574
13575 update_test_language_settings(&mut cx, |settings| {
13576 settings.defaults.completions = Some(CompletionSettingsContent {
13577 lsp_insert_mode: Some(lsp_insert_mode),
13578 words: Some(WordsCompletionMode::Disabled),
13579 words_min_length: Some(0),
13580 ..Default::default()
13581 });
13582 });
13583
13584 cx.set_state(&run.initial_state);
13585 cx.update_editor(|editor, window, cx| {
13586 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13587 });
13588
13589 let counter = Arc::new(AtomicUsize::new(0));
13590 handle_completion_request_with_insert_and_replace(
13591 &mut cx,
13592 &run.buffer_marked_text,
13593 vec![(run.completion_label, run.completion_text)],
13594 counter.clone(),
13595 )
13596 .await;
13597 cx.condition(|editor, _| editor.context_menu_visible())
13598 .await;
13599 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13600
13601 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13602 editor
13603 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13604 .unwrap()
13605 });
13606 cx.assert_editor_state(&expected_text);
13607 handle_resolve_completion_request(&mut cx, None).await;
13608 apply_additional_edits.await.unwrap();
13609 }
13610 }
13611}
13612
13613#[gpui::test]
13614async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13615 init_test(cx, |_| {});
13616 let mut cx = EditorLspTestContext::new_rust(
13617 lsp::ServerCapabilities {
13618 completion_provider: Some(lsp::CompletionOptions {
13619 resolve_provider: Some(true),
13620 ..Default::default()
13621 }),
13622 ..Default::default()
13623 },
13624 cx,
13625 )
13626 .await;
13627
13628 let initial_state = "SubˇError";
13629 let buffer_marked_text = "<Sub|Error>";
13630 let completion_text = "SubscriptionError";
13631 let expected_with_insert_mode = "SubscriptionErrorˇError";
13632 let expected_with_replace_mode = "SubscriptionErrorˇ";
13633
13634 update_test_language_settings(&mut cx, |settings| {
13635 settings.defaults.completions = Some(CompletionSettingsContent {
13636 words: Some(WordsCompletionMode::Disabled),
13637 words_min_length: Some(0),
13638 // set the opposite here to ensure that the action is overriding the default behavior
13639 lsp_insert_mode: Some(LspInsertMode::Insert),
13640 ..Default::default()
13641 });
13642 });
13643
13644 cx.set_state(initial_state);
13645 cx.update_editor(|editor, window, cx| {
13646 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13647 });
13648
13649 let counter = Arc::new(AtomicUsize::new(0));
13650 handle_completion_request_with_insert_and_replace(
13651 &mut cx,
13652 buffer_marked_text,
13653 vec![(completion_text, completion_text)],
13654 counter.clone(),
13655 )
13656 .await;
13657 cx.condition(|editor, _| editor.context_menu_visible())
13658 .await;
13659 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13660
13661 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13662 editor
13663 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13664 .unwrap()
13665 });
13666 cx.assert_editor_state(expected_with_replace_mode);
13667 handle_resolve_completion_request(&mut cx, None).await;
13668 apply_additional_edits.await.unwrap();
13669
13670 update_test_language_settings(&mut cx, |settings| {
13671 settings.defaults.completions = Some(CompletionSettingsContent {
13672 words: Some(WordsCompletionMode::Disabled),
13673 words_min_length: Some(0),
13674 // set the opposite here to ensure that the action is overriding the default behavior
13675 lsp_insert_mode: Some(LspInsertMode::Replace),
13676 ..Default::default()
13677 });
13678 });
13679
13680 cx.set_state(initial_state);
13681 cx.update_editor(|editor, window, cx| {
13682 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13683 });
13684 handle_completion_request_with_insert_and_replace(
13685 &mut cx,
13686 buffer_marked_text,
13687 vec![(completion_text, completion_text)],
13688 counter.clone(),
13689 )
13690 .await;
13691 cx.condition(|editor, _| editor.context_menu_visible())
13692 .await;
13693 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13694
13695 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13696 editor
13697 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13698 .unwrap()
13699 });
13700 cx.assert_editor_state(expected_with_insert_mode);
13701 handle_resolve_completion_request(&mut cx, None).await;
13702 apply_additional_edits.await.unwrap();
13703}
13704
13705#[gpui::test]
13706async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13707 init_test(cx, |_| {});
13708 let mut cx = EditorLspTestContext::new_rust(
13709 lsp::ServerCapabilities {
13710 completion_provider: Some(lsp::CompletionOptions {
13711 resolve_provider: Some(true),
13712 ..Default::default()
13713 }),
13714 ..Default::default()
13715 },
13716 cx,
13717 )
13718 .await;
13719
13720 // scenario: surrounding text matches completion text
13721 let completion_text = "to_offset";
13722 let initial_state = indoc! {"
13723 1. buf.to_offˇsuffix
13724 2. buf.to_offˇsuf
13725 3. buf.to_offˇfix
13726 4. buf.to_offˇ
13727 5. into_offˇensive
13728 6. ˇsuffix
13729 7. let ˇ //
13730 8. aaˇzz
13731 9. buf.to_off«zzzzzˇ»suffix
13732 10. buf.«ˇzzzzz»suffix
13733 11. to_off«ˇzzzzz»
13734
13735 buf.to_offˇsuffix // newest cursor
13736 "};
13737 let completion_marked_buffer = indoc! {"
13738 1. buf.to_offsuffix
13739 2. buf.to_offsuf
13740 3. buf.to_offfix
13741 4. buf.to_off
13742 5. into_offensive
13743 6. suffix
13744 7. let //
13745 8. aazz
13746 9. buf.to_offzzzzzsuffix
13747 10. buf.zzzzzsuffix
13748 11. to_offzzzzz
13749
13750 buf.<to_off|suffix> // newest cursor
13751 "};
13752 let expected = indoc! {"
13753 1. buf.to_offsetˇ
13754 2. buf.to_offsetˇsuf
13755 3. buf.to_offsetˇfix
13756 4. buf.to_offsetˇ
13757 5. into_offsetˇensive
13758 6. to_offsetˇsuffix
13759 7. let to_offsetˇ //
13760 8. aato_offsetˇzz
13761 9. buf.to_offsetˇ
13762 10. buf.to_offsetˇsuffix
13763 11. to_offsetˇ
13764
13765 buf.to_offsetˇ // newest cursor
13766 "};
13767 cx.set_state(initial_state);
13768 cx.update_editor(|editor, window, cx| {
13769 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13770 });
13771 handle_completion_request_with_insert_and_replace(
13772 &mut cx,
13773 completion_marked_buffer,
13774 vec![(completion_text, completion_text)],
13775 Arc::new(AtomicUsize::new(0)),
13776 )
13777 .await;
13778 cx.condition(|editor, _| editor.context_menu_visible())
13779 .await;
13780 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13781 editor
13782 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13783 .unwrap()
13784 });
13785 cx.assert_editor_state(expected);
13786 handle_resolve_completion_request(&mut cx, None).await;
13787 apply_additional_edits.await.unwrap();
13788
13789 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13790 let completion_text = "foo_and_bar";
13791 let initial_state = indoc! {"
13792 1. ooanbˇ
13793 2. zooanbˇ
13794 3. ooanbˇz
13795 4. zooanbˇz
13796 5. ooanˇ
13797 6. oanbˇ
13798
13799 ooanbˇ
13800 "};
13801 let completion_marked_buffer = indoc! {"
13802 1. ooanb
13803 2. zooanb
13804 3. ooanbz
13805 4. zooanbz
13806 5. ooan
13807 6. oanb
13808
13809 <ooanb|>
13810 "};
13811 let expected = indoc! {"
13812 1. foo_and_barˇ
13813 2. zfoo_and_barˇ
13814 3. foo_and_barˇz
13815 4. zfoo_and_barˇz
13816 5. ooanfoo_and_barˇ
13817 6. oanbfoo_and_barˇ
13818
13819 foo_and_barˇ
13820 "};
13821 cx.set_state(initial_state);
13822 cx.update_editor(|editor, window, cx| {
13823 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13824 });
13825 handle_completion_request_with_insert_and_replace(
13826 &mut cx,
13827 completion_marked_buffer,
13828 vec![(completion_text, completion_text)],
13829 Arc::new(AtomicUsize::new(0)),
13830 )
13831 .await;
13832 cx.condition(|editor, _| editor.context_menu_visible())
13833 .await;
13834 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13835 editor
13836 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13837 .unwrap()
13838 });
13839 cx.assert_editor_state(expected);
13840 handle_resolve_completion_request(&mut cx, None).await;
13841 apply_additional_edits.await.unwrap();
13842
13843 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13844 // (expects the same as if it was inserted at the end)
13845 let completion_text = "foo_and_bar";
13846 let initial_state = indoc! {"
13847 1. ooˇanb
13848 2. zooˇanb
13849 3. ooˇanbz
13850 4. zooˇanbz
13851
13852 ooˇanb
13853 "};
13854 let completion_marked_buffer = indoc! {"
13855 1. ooanb
13856 2. zooanb
13857 3. ooanbz
13858 4. zooanbz
13859
13860 <oo|anb>
13861 "};
13862 let expected = indoc! {"
13863 1. foo_and_barˇ
13864 2. zfoo_and_barˇ
13865 3. foo_and_barˇz
13866 4. zfoo_and_barˇz
13867
13868 foo_and_barˇ
13869 "};
13870 cx.set_state(initial_state);
13871 cx.update_editor(|editor, window, cx| {
13872 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13873 });
13874 handle_completion_request_with_insert_and_replace(
13875 &mut cx,
13876 completion_marked_buffer,
13877 vec![(completion_text, completion_text)],
13878 Arc::new(AtomicUsize::new(0)),
13879 )
13880 .await;
13881 cx.condition(|editor, _| editor.context_menu_visible())
13882 .await;
13883 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13884 editor
13885 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13886 .unwrap()
13887 });
13888 cx.assert_editor_state(expected);
13889 handle_resolve_completion_request(&mut cx, None).await;
13890 apply_additional_edits.await.unwrap();
13891}
13892
13893// This used to crash
13894#[gpui::test]
13895async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13896 init_test(cx, |_| {});
13897
13898 let buffer_text = indoc! {"
13899 fn main() {
13900 10.satu;
13901
13902 //
13903 // separate cursors so they open in different excerpts (manually reproducible)
13904 //
13905
13906 10.satu20;
13907 }
13908 "};
13909 let multibuffer_text_with_selections = indoc! {"
13910 fn main() {
13911 10.satuˇ;
13912
13913 //
13914
13915 //
13916
13917 10.satuˇ20;
13918 }
13919 "};
13920 let expected_multibuffer = indoc! {"
13921 fn main() {
13922 10.saturating_sub()ˇ;
13923
13924 //
13925
13926 //
13927
13928 10.saturating_sub()ˇ;
13929 }
13930 "};
13931
13932 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13933 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13934
13935 let fs = FakeFs::new(cx.executor());
13936 fs.insert_tree(
13937 path!("/a"),
13938 json!({
13939 "main.rs": buffer_text,
13940 }),
13941 )
13942 .await;
13943
13944 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13945 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13946 language_registry.add(rust_lang());
13947 let mut fake_servers = language_registry.register_fake_lsp(
13948 "Rust",
13949 FakeLspAdapter {
13950 capabilities: lsp::ServerCapabilities {
13951 completion_provider: Some(lsp::CompletionOptions {
13952 resolve_provider: None,
13953 ..lsp::CompletionOptions::default()
13954 }),
13955 ..lsp::ServerCapabilities::default()
13956 },
13957 ..FakeLspAdapter::default()
13958 },
13959 );
13960 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13961 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13962 let buffer = project
13963 .update(cx, |project, cx| {
13964 project.open_local_buffer(path!("/a/main.rs"), cx)
13965 })
13966 .await
13967 .unwrap();
13968
13969 let multi_buffer = cx.new(|cx| {
13970 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13971 multi_buffer.push_excerpts(
13972 buffer.clone(),
13973 [ExcerptRange::new(0..first_excerpt_end)],
13974 cx,
13975 );
13976 multi_buffer.push_excerpts(
13977 buffer.clone(),
13978 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13979 cx,
13980 );
13981 multi_buffer
13982 });
13983
13984 let editor = workspace
13985 .update(cx, |_, window, cx| {
13986 cx.new(|cx| {
13987 Editor::new(
13988 EditorMode::Full {
13989 scale_ui_elements_with_buffer_font_size: false,
13990 show_active_line_background: false,
13991 sized_by_content: false,
13992 },
13993 multi_buffer.clone(),
13994 Some(project.clone()),
13995 window,
13996 cx,
13997 )
13998 })
13999 })
14000 .unwrap();
14001
14002 let pane = workspace
14003 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14004 .unwrap();
14005 pane.update_in(cx, |pane, window, cx| {
14006 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14007 });
14008
14009 let fake_server = fake_servers.next().await.unwrap();
14010
14011 editor.update_in(cx, |editor, window, cx| {
14012 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14013 s.select_ranges([
14014 Point::new(1, 11)..Point::new(1, 11),
14015 Point::new(7, 11)..Point::new(7, 11),
14016 ])
14017 });
14018
14019 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14020 });
14021
14022 editor.update_in(cx, |editor, window, cx| {
14023 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14024 });
14025
14026 fake_server
14027 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14028 let completion_item = lsp::CompletionItem {
14029 label: "saturating_sub()".into(),
14030 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14031 lsp::InsertReplaceEdit {
14032 new_text: "saturating_sub()".to_owned(),
14033 insert: lsp::Range::new(
14034 lsp::Position::new(7, 7),
14035 lsp::Position::new(7, 11),
14036 ),
14037 replace: lsp::Range::new(
14038 lsp::Position::new(7, 7),
14039 lsp::Position::new(7, 13),
14040 ),
14041 },
14042 )),
14043 ..lsp::CompletionItem::default()
14044 };
14045
14046 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14047 })
14048 .next()
14049 .await
14050 .unwrap();
14051
14052 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14053 .await;
14054
14055 editor
14056 .update_in(cx, |editor, window, cx| {
14057 editor
14058 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14059 .unwrap()
14060 })
14061 .await
14062 .unwrap();
14063
14064 editor.update(cx, |editor, cx| {
14065 assert_text_with_selections(editor, expected_multibuffer, cx);
14066 })
14067}
14068
14069#[gpui::test]
14070async fn test_completion(cx: &mut TestAppContext) {
14071 init_test(cx, |_| {});
14072
14073 let mut cx = EditorLspTestContext::new_rust(
14074 lsp::ServerCapabilities {
14075 completion_provider: Some(lsp::CompletionOptions {
14076 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14077 resolve_provider: Some(true),
14078 ..Default::default()
14079 }),
14080 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14081 ..Default::default()
14082 },
14083 cx,
14084 )
14085 .await;
14086 let counter = Arc::new(AtomicUsize::new(0));
14087
14088 cx.set_state(indoc! {"
14089 oneˇ
14090 two
14091 three
14092 "});
14093 cx.simulate_keystroke(".");
14094 handle_completion_request(
14095 indoc! {"
14096 one.|<>
14097 two
14098 three
14099 "},
14100 vec!["first_completion", "second_completion"],
14101 true,
14102 counter.clone(),
14103 &mut cx,
14104 )
14105 .await;
14106 cx.condition(|editor, _| editor.context_menu_visible())
14107 .await;
14108 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14109
14110 let _handler = handle_signature_help_request(
14111 &mut cx,
14112 lsp::SignatureHelp {
14113 signatures: vec![lsp::SignatureInformation {
14114 label: "test signature".to_string(),
14115 documentation: None,
14116 parameters: Some(vec![lsp::ParameterInformation {
14117 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14118 documentation: None,
14119 }]),
14120 active_parameter: None,
14121 }],
14122 active_signature: None,
14123 active_parameter: None,
14124 },
14125 );
14126 cx.update_editor(|editor, window, cx| {
14127 assert!(
14128 !editor.signature_help_state.is_shown(),
14129 "No signature help was called for"
14130 );
14131 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14132 });
14133 cx.run_until_parked();
14134 cx.update_editor(|editor, _, _| {
14135 assert!(
14136 !editor.signature_help_state.is_shown(),
14137 "No signature help should be shown when completions menu is open"
14138 );
14139 });
14140
14141 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14142 editor.context_menu_next(&Default::default(), window, cx);
14143 editor
14144 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14145 .unwrap()
14146 });
14147 cx.assert_editor_state(indoc! {"
14148 one.second_completionˇ
14149 two
14150 three
14151 "});
14152
14153 handle_resolve_completion_request(
14154 &mut cx,
14155 Some(vec![
14156 (
14157 //This overlaps with the primary completion edit which is
14158 //misbehavior from the LSP spec, test that we filter it out
14159 indoc! {"
14160 one.second_ˇcompletion
14161 two
14162 threeˇ
14163 "},
14164 "overlapping additional edit",
14165 ),
14166 (
14167 indoc! {"
14168 one.second_completion
14169 two
14170 threeˇ
14171 "},
14172 "\nadditional edit",
14173 ),
14174 ]),
14175 )
14176 .await;
14177 apply_additional_edits.await.unwrap();
14178 cx.assert_editor_state(indoc! {"
14179 one.second_completionˇ
14180 two
14181 three
14182 additional edit
14183 "});
14184
14185 cx.set_state(indoc! {"
14186 one.second_completion
14187 twoˇ
14188 threeˇ
14189 additional edit
14190 "});
14191 cx.simulate_keystroke(" ");
14192 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14193 cx.simulate_keystroke("s");
14194 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14195
14196 cx.assert_editor_state(indoc! {"
14197 one.second_completion
14198 two sˇ
14199 three sˇ
14200 additional edit
14201 "});
14202 handle_completion_request(
14203 indoc! {"
14204 one.second_completion
14205 two s
14206 three <s|>
14207 additional edit
14208 "},
14209 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14210 true,
14211 counter.clone(),
14212 &mut cx,
14213 )
14214 .await;
14215 cx.condition(|editor, _| editor.context_menu_visible())
14216 .await;
14217 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14218
14219 cx.simulate_keystroke("i");
14220
14221 handle_completion_request(
14222 indoc! {"
14223 one.second_completion
14224 two si
14225 three <si|>
14226 additional edit
14227 "},
14228 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14229 true,
14230 counter.clone(),
14231 &mut cx,
14232 )
14233 .await;
14234 cx.condition(|editor, _| editor.context_menu_visible())
14235 .await;
14236 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14237
14238 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14239 editor
14240 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14241 .unwrap()
14242 });
14243 cx.assert_editor_state(indoc! {"
14244 one.second_completion
14245 two sixth_completionˇ
14246 three sixth_completionˇ
14247 additional edit
14248 "});
14249
14250 apply_additional_edits.await.unwrap();
14251
14252 update_test_language_settings(&mut cx, |settings| {
14253 settings.defaults.show_completions_on_input = Some(false);
14254 });
14255 cx.set_state("editorˇ");
14256 cx.simulate_keystroke(".");
14257 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14258 cx.simulate_keystrokes("c l o");
14259 cx.assert_editor_state("editor.cloˇ");
14260 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14261 cx.update_editor(|editor, window, cx| {
14262 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14263 });
14264 handle_completion_request(
14265 "editor.<clo|>",
14266 vec!["close", "clobber"],
14267 true,
14268 counter.clone(),
14269 &mut cx,
14270 )
14271 .await;
14272 cx.condition(|editor, _| editor.context_menu_visible())
14273 .await;
14274 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14275
14276 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14277 editor
14278 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14279 .unwrap()
14280 });
14281 cx.assert_editor_state("editor.clobberˇ");
14282 handle_resolve_completion_request(&mut cx, None).await;
14283 apply_additional_edits.await.unwrap();
14284}
14285
14286#[gpui::test]
14287async fn test_completion_reuse(cx: &mut TestAppContext) {
14288 init_test(cx, |_| {});
14289
14290 let mut cx = EditorLspTestContext::new_rust(
14291 lsp::ServerCapabilities {
14292 completion_provider: Some(lsp::CompletionOptions {
14293 trigger_characters: Some(vec![".".to_string()]),
14294 ..Default::default()
14295 }),
14296 ..Default::default()
14297 },
14298 cx,
14299 )
14300 .await;
14301
14302 let counter = Arc::new(AtomicUsize::new(0));
14303 cx.set_state("objˇ");
14304 cx.simulate_keystroke(".");
14305
14306 // Initial completion request returns complete results
14307 let is_incomplete = false;
14308 handle_completion_request(
14309 "obj.|<>",
14310 vec!["a", "ab", "abc"],
14311 is_incomplete,
14312 counter.clone(),
14313 &mut cx,
14314 )
14315 .await;
14316 cx.run_until_parked();
14317 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14318 cx.assert_editor_state("obj.ˇ");
14319 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14320
14321 // Type "a" - filters existing completions
14322 cx.simulate_keystroke("a");
14323 cx.run_until_parked();
14324 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14325 cx.assert_editor_state("obj.aˇ");
14326 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14327
14328 // Type "b" - filters existing completions
14329 cx.simulate_keystroke("b");
14330 cx.run_until_parked();
14331 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14332 cx.assert_editor_state("obj.abˇ");
14333 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14334
14335 // Type "c" - filters existing completions
14336 cx.simulate_keystroke("c");
14337 cx.run_until_parked();
14338 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14339 cx.assert_editor_state("obj.abcˇ");
14340 check_displayed_completions(vec!["abc"], &mut cx);
14341
14342 // Backspace to delete "c" - filters existing completions
14343 cx.update_editor(|editor, window, cx| {
14344 editor.backspace(&Backspace, window, cx);
14345 });
14346 cx.run_until_parked();
14347 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14348 cx.assert_editor_state("obj.abˇ");
14349 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14350
14351 // Moving cursor to the left dismisses menu.
14352 cx.update_editor(|editor, window, cx| {
14353 editor.move_left(&MoveLeft, window, cx);
14354 });
14355 cx.run_until_parked();
14356 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14357 cx.assert_editor_state("obj.aˇb");
14358 cx.update_editor(|editor, _, _| {
14359 assert_eq!(editor.context_menu_visible(), false);
14360 });
14361
14362 // Type "b" - new request
14363 cx.simulate_keystroke("b");
14364 let is_incomplete = false;
14365 handle_completion_request(
14366 "obj.<ab|>a",
14367 vec!["ab", "abc"],
14368 is_incomplete,
14369 counter.clone(),
14370 &mut cx,
14371 )
14372 .await;
14373 cx.run_until_parked();
14374 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14375 cx.assert_editor_state("obj.abˇb");
14376 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14377
14378 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14379 cx.update_editor(|editor, window, cx| {
14380 editor.backspace(&Backspace, window, cx);
14381 });
14382 let is_incomplete = false;
14383 handle_completion_request(
14384 "obj.<a|>b",
14385 vec!["a", "ab", "abc"],
14386 is_incomplete,
14387 counter.clone(),
14388 &mut cx,
14389 )
14390 .await;
14391 cx.run_until_parked();
14392 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14393 cx.assert_editor_state("obj.aˇb");
14394 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14395
14396 // Backspace to delete "a" - dismisses menu.
14397 cx.update_editor(|editor, window, cx| {
14398 editor.backspace(&Backspace, window, cx);
14399 });
14400 cx.run_until_parked();
14401 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14402 cx.assert_editor_state("obj.ˇb");
14403 cx.update_editor(|editor, _, _| {
14404 assert_eq!(editor.context_menu_visible(), false);
14405 });
14406}
14407
14408#[gpui::test]
14409async fn test_word_completion(cx: &mut TestAppContext) {
14410 let lsp_fetch_timeout_ms = 10;
14411 init_test(cx, |language_settings| {
14412 language_settings.defaults.completions = Some(CompletionSettingsContent {
14413 words_min_length: Some(0),
14414 lsp_fetch_timeout_ms: Some(10),
14415 lsp_insert_mode: Some(LspInsertMode::Insert),
14416 ..Default::default()
14417 });
14418 });
14419
14420 let mut cx = EditorLspTestContext::new_rust(
14421 lsp::ServerCapabilities {
14422 completion_provider: Some(lsp::CompletionOptions {
14423 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14424 ..lsp::CompletionOptions::default()
14425 }),
14426 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14427 ..lsp::ServerCapabilities::default()
14428 },
14429 cx,
14430 )
14431 .await;
14432
14433 let throttle_completions = Arc::new(AtomicBool::new(false));
14434
14435 let lsp_throttle_completions = throttle_completions.clone();
14436 let _completion_requests_handler =
14437 cx.lsp
14438 .server
14439 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14440 let lsp_throttle_completions = lsp_throttle_completions.clone();
14441 let cx = cx.clone();
14442 async move {
14443 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14444 cx.background_executor()
14445 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14446 .await;
14447 }
14448 Ok(Some(lsp::CompletionResponse::Array(vec![
14449 lsp::CompletionItem {
14450 label: "first".into(),
14451 ..lsp::CompletionItem::default()
14452 },
14453 lsp::CompletionItem {
14454 label: "last".into(),
14455 ..lsp::CompletionItem::default()
14456 },
14457 ])))
14458 }
14459 });
14460
14461 cx.set_state(indoc! {"
14462 oneˇ
14463 two
14464 three
14465 "});
14466 cx.simulate_keystroke(".");
14467 cx.executor().run_until_parked();
14468 cx.condition(|editor, _| editor.context_menu_visible())
14469 .await;
14470 cx.update_editor(|editor, window, cx| {
14471 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14472 {
14473 assert_eq!(
14474 completion_menu_entries(menu),
14475 &["first", "last"],
14476 "When LSP server is fast to reply, no fallback word completions are used"
14477 );
14478 } else {
14479 panic!("expected completion menu to be open");
14480 }
14481 editor.cancel(&Cancel, window, cx);
14482 });
14483 cx.executor().run_until_parked();
14484 cx.condition(|editor, _| !editor.context_menu_visible())
14485 .await;
14486
14487 throttle_completions.store(true, atomic::Ordering::Release);
14488 cx.simulate_keystroke(".");
14489 cx.executor()
14490 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14491 cx.executor().run_until_parked();
14492 cx.condition(|editor, _| editor.context_menu_visible())
14493 .await;
14494 cx.update_editor(|editor, _, _| {
14495 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14496 {
14497 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14498 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14499 } else {
14500 panic!("expected completion menu to be open");
14501 }
14502 });
14503}
14504
14505#[gpui::test]
14506async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14507 init_test(cx, |language_settings| {
14508 language_settings.defaults.completions = Some(CompletionSettingsContent {
14509 words: Some(WordsCompletionMode::Enabled),
14510 words_min_length: Some(0),
14511 lsp_insert_mode: Some(LspInsertMode::Insert),
14512 ..Default::default()
14513 });
14514 });
14515
14516 let mut cx = EditorLspTestContext::new_rust(
14517 lsp::ServerCapabilities {
14518 completion_provider: Some(lsp::CompletionOptions {
14519 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14520 ..lsp::CompletionOptions::default()
14521 }),
14522 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14523 ..lsp::ServerCapabilities::default()
14524 },
14525 cx,
14526 )
14527 .await;
14528
14529 let _completion_requests_handler =
14530 cx.lsp
14531 .server
14532 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14533 Ok(Some(lsp::CompletionResponse::Array(vec![
14534 lsp::CompletionItem {
14535 label: "first".into(),
14536 ..lsp::CompletionItem::default()
14537 },
14538 lsp::CompletionItem {
14539 label: "last".into(),
14540 ..lsp::CompletionItem::default()
14541 },
14542 ])))
14543 });
14544
14545 cx.set_state(indoc! {"ˇ
14546 first
14547 last
14548 second
14549 "});
14550 cx.simulate_keystroke(".");
14551 cx.executor().run_until_parked();
14552 cx.condition(|editor, _| editor.context_menu_visible())
14553 .await;
14554 cx.update_editor(|editor, _, _| {
14555 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14556 {
14557 assert_eq!(
14558 completion_menu_entries(menu),
14559 &["first", "last", "second"],
14560 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14561 );
14562 } else {
14563 panic!("expected completion menu to be open");
14564 }
14565 });
14566}
14567
14568#[gpui::test]
14569async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14570 init_test(cx, |language_settings| {
14571 language_settings.defaults.completions = Some(CompletionSettingsContent {
14572 words: Some(WordsCompletionMode::Disabled),
14573 words_min_length: Some(0),
14574 lsp_insert_mode: Some(LspInsertMode::Insert),
14575 ..Default::default()
14576 });
14577 });
14578
14579 let mut cx = EditorLspTestContext::new_rust(
14580 lsp::ServerCapabilities {
14581 completion_provider: Some(lsp::CompletionOptions {
14582 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14583 ..lsp::CompletionOptions::default()
14584 }),
14585 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14586 ..lsp::ServerCapabilities::default()
14587 },
14588 cx,
14589 )
14590 .await;
14591
14592 let _completion_requests_handler =
14593 cx.lsp
14594 .server
14595 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14596 panic!("LSP completions should not be queried when dealing with word completions")
14597 });
14598
14599 cx.set_state(indoc! {"ˇ
14600 first
14601 last
14602 second
14603 "});
14604 cx.update_editor(|editor, window, cx| {
14605 editor.show_word_completions(&ShowWordCompletions, window, cx);
14606 });
14607 cx.executor().run_until_parked();
14608 cx.condition(|editor, _| editor.context_menu_visible())
14609 .await;
14610 cx.update_editor(|editor, _, _| {
14611 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14612 {
14613 assert_eq!(
14614 completion_menu_entries(menu),
14615 &["first", "last", "second"],
14616 "`ShowWordCompletions` action should show word completions"
14617 );
14618 } else {
14619 panic!("expected completion menu to be open");
14620 }
14621 });
14622
14623 cx.simulate_keystroke("l");
14624 cx.executor().run_until_parked();
14625 cx.condition(|editor, _| editor.context_menu_visible())
14626 .await;
14627 cx.update_editor(|editor, _, _| {
14628 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14629 {
14630 assert_eq!(
14631 completion_menu_entries(menu),
14632 &["last"],
14633 "After showing word completions, further editing should filter them and not query the LSP"
14634 );
14635 } else {
14636 panic!("expected completion menu to be open");
14637 }
14638 });
14639}
14640
14641#[gpui::test]
14642async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14643 init_test(cx, |language_settings| {
14644 language_settings.defaults.completions = Some(CompletionSettingsContent {
14645 words_min_length: Some(0),
14646 lsp: Some(false),
14647 lsp_insert_mode: Some(LspInsertMode::Insert),
14648 ..Default::default()
14649 });
14650 });
14651
14652 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14653
14654 cx.set_state(indoc! {"ˇ
14655 0_usize
14656 let
14657 33
14658 4.5f32
14659 "});
14660 cx.update_editor(|editor, window, cx| {
14661 editor.show_completions(&ShowCompletions::default(), window, cx);
14662 });
14663 cx.executor().run_until_parked();
14664 cx.condition(|editor, _| editor.context_menu_visible())
14665 .await;
14666 cx.update_editor(|editor, window, cx| {
14667 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14668 {
14669 assert_eq!(
14670 completion_menu_entries(menu),
14671 &["let"],
14672 "With no digits in the completion query, no digits should be in the word completions"
14673 );
14674 } else {
14675 panic!("expected completion menu to be open");
14676 }
14677 editor.cancel(&Cancel, window, cx);
14678 });
14679
14680 cx.set_state(indoc! {"3ˇ
14681 0_usize
14682 let
14683 3
14684 33.35f32
14685 "});
14686 cx.update_editor(|editor, window, cx| {
14687 editor.show_completions(&ShowCompletions::default(), window, cx);
14688 });
14689 cx.executor().run_until_parked();
14690 cx.condition(|editor, _| editor.context_menu_visible())
14691 .await;
14692 cx.update_editor(|editor, _, _| {
14693 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14694 {
14695 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14696 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14697 } else {
14698 panic!("expected completion menu to be open");
14699 }
14700 });
14701}
14702
14703#[gpui::test]
14704async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14705 init_test(cx, |language_settings| {
14706 language_settings.defaults.completions = Some(CompletionSettingsContent {
14707 words: Some(WordsCompletionMode::Enabled),
14708 words_min_length: Some(3),
14709 lsp_insert_mode: Some(LspInsertMode::Insert),
14710 ..Default::default()
14711 });
14712 });
14713
14714 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14715 cx.set_state(indoc! {"ˇ
14716 wow
14717 wowen
14718 wowser
14719 "});
14720 cx.simulate_keystroke("w");
14721 cx.executor().run_until_parked();
14722 cx.update_editor(|editor, _, _| {
14723 if editor.context_menu.borrow_mut().is_some() {
14724 panic!(
14725 "expected completion menu to be hidden, as words completion threshold is not met"
14726 );
14727 }
14728 });
14729
14730 cx.update_editor(|editor, window, cx| {
14731 editor.show_word_completions(&ShowWordCompletions, window, cx);
14732 });
14733 cx.executor().run_until_parked();
14734 cx.update_editor(|editor, window, cx| {
14735 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14736 {
14737 assert_eq!(completion_menu_entries(menu), &["wowser", "wowen", "wow"], "Even though the threshold is not met, invoking word completions with an action should provide the completions");
14738 } else {
14739 panic!("expected completion menu to be open after the word completions are called with an action");
14740 }
14741
14742 editor.cancel(&Cancel, window, cx);
14743 });
14744 cx.update_editor(|editor, _, _| {
14745 if editor.context_menu.borrow_mut().is_some() {
14746 panic!("expected completion menu to be hidden after canceling");
14747 }
14748 });
14749
14750 cx.simulate_keystroke("o");
14751 cx.executor().run_until_parked();
14752 cx.update_editor(|editor, _, _| {
14753 if editor.context_menu.borrow_mut().is_some() {
14754 panic!(
14755 "expected completion menu to be hidden, as words completion threshold is not met still"
14756 );
14757 }
14758 });
14759
14760 cx.simulate_keystroke("w");
14761 cx.executor().run_until_parked();
14762 cx.update_editor(|editor, _, _| {
14763 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14764 {
14765 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14766 } else {
14767 panic!("expected completion menu to be open after the word completions threshold is met");
14768 }
14769 });
14770}
14771
14772#[gpui::test]
14773async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14774 init_test(cx, |language_settings| {
14775 language_settings.defaults.completions = Some(CompletionSettingsContent {
14776 words: Some(WordsCompletionMode::Enabled),
14777 words_min_length: Some(0),
14778 lsp_insert_mode: Some(LspInsertMode::Insert),
14779 ..Default::default()
14780 });
14781 });
14782
14783 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14784 cx.update_editor(|editor, _, _| {
14785 editor.disable_word_completions();
14786 });
14787 cx.set_state(indoc! {"ˇ
14788 wow
14789 wowen
14790 wowser
14791 "});
14792 cx.simulate_keystroke("w");
14793 cx.executor().run_until_parked();
14794 cx.update_editor(|editor, _, _| {
14795 if editor.context_menu.borrow_mut().is_some() {
14796 panic!(
14797 "expected completion menu to be hidden, as words completion are disabled for this editor"
14798 );
14799 }
14800 });
14801
14802 cx.update_editor(|editor, window, cx| {
14803 editor.show_word_completions(&ShowWordCompletions, window, cx);
14804 });
14805 cx.executor().run_until_parked();
14806 cx.update_editor(|editor, _, _| {
14807 if editor.context_menu.borrow_mut().is_some() {
14808 panic!(
14809 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14810 );
14811 }
14812 });
14813}
14814
14815fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14816 let position = || lsp::Position {
14817 line: params.text_document_position.position.line,
14818 character: params.text_document_position.position.character,
14819 };
14820 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14821 range: lsp::Range {
14822 start: position(),
14823 end: position(),
14824 },
14825 new_text: text.to_string(),
14826 }))
14827}
14828
14829#[gpui::test]
14830async fn test_multiline_completion(cx: &mut TestAppContext) {
14831 init_test(cx, |_| {});
14832
14833 let fs = FakeFs::new(cx.executor());
14834 fs.insert_tree(
14835 path!("/a"),
14836 json!({
14837 "main.ts": "a",
14838 }),
14839 )
14840 .await;
14841
14842 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14843 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14844 let typescript_language = Arc::new(Language::new(
14845 LanguageConfig {
14846 name: "TypeScript".into(),
14847 matcher: LanguageMatcher {
14848 path_suffixes: vec!["ts".to_string()],
14849 ..LanguageMatcher::default()
14850 },
14851 line_comments: vec!["// ".into()],
14852 ..LanguageConfig::default()
14853 },
14854 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14855 ));
14856 language_registry.add(typescript_language.clone());
14857 let mut fake_servers = language_registry.register_fake_lsp(
14858 "TypeScript",
14859 FakeLspAdapter {
14860 capabilities: lsp::ServerCapabilities {
14861 completion_provider: Some(lsp::CompletionOptions {
14862 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14863 ..lsp::CompletionOptions::default()
14864 }),
14865 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14866 ..lsp::ServerCapabilities::default()
14867 },
14868 // Emulate vtsls label generation
14869 label_for_completion: Some(Box::new(|item, _| {
14870 let text = if let Some(description) = item
14871 .label_details
14872 .as_ref()
14873 .and_then(|label_details| label_details.description.as_ref())
14874 {
14875 format!("{} {}", item.label, description)
14876 } else if let Some(detail) = &item.detail {
14877 format!("{} {}", item.label, detail)
14878 } else {
14879 item.label.clone()
14880 };
14881 Some(language::CodeLabel::plain(text, None))
14882 })),
14883 ..FakeLspAdapter::default()
14884 },
14885 );
14886 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14887 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14888 let worktree_id = workspace
14889 .update(cx, |workspace, _window, cx| {
14890 workspace.project().update(cx, |project, cx| {
14891 project.worktrees(cx).next().unwrap().read(cx).id()
14892 })
14893 })
14894 .unwrap();
14895 let _buffer = project
14896 .update(cx, |project, cx| {
14897 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14898 })
14899 .await
14900 .unwrap();
14901 let editor = workspace
14902 .update(cx, |workspace, window, cx| {
14903 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
14904 })
14905 .unwrap()
14906 .await
14907 .unwrap()
14908 .downcast::<Editor>()
14909 .unwrap();
14910 let fake_server = fake_servers.next().await.unwrap();
14911
14912 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14913 let multiline_label_2 = "a\nb\nc\n";
14914 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14915 let multiline_description = "d\ne\nf\n";
14916 let multiline_detail_2 = "g\nh\ni\n";
14917
14918 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14919 move |params, _| async move {
14920 Ok(Some(lsp::CompletionResponse::Array(vec![
14921 lsp::CompletionItem {
14922 label: multiline_label.to_string(),
14923 text_edit: gen_text_edit(¶ms, "new_text_1"),
14924 ..lsp::CompletionItem::default()
14925 },
14926 lsp::CompletionItem {
14927 label: "single line label 1".to_string(),
14928 detail: Some(multiline_detail.to_string()),
14929 text_edit: gen_text_edit(¶ms, "new_text_2"),
14930 ..lsp::CompletionItem::default()
14931 },
14932 lsp::CompletionItem {
14933 label: "single line label 2".to_string(),
14934 label_details: Some(lsp::CompletionItemLabelDetails {
14935 description: Some(multiline_description.to_string()),
14936 detail: None,
14937 }),
14938 text_edit: gen_text_edit(¶ms, "new_text_2"),
14939 ..lsp::CompletionItem::default()
14940 },
14941 lsp::CompletionItem {
14942 label: multiline_label_2.to_string(),
14943 detail: Some(multiline_detail_2.to_string()),
14944 text_edit: gen_text_edit(¶ms, "new_text_3"),
14945 ..lsp::CompletionItem::default()
14946 },
14947 lsp::CompletionItem {
14948 label: "Label with many spaces and \t but without newlines".to_string(),
14949 detail: Some(
14950 "Details with many spaces and \t but without newlines".to_string(),
14951 ),
14952 text_edit: gen_text_edit(¶ms, "new_text_4"),
14953 ..lsp::CompletionItem::default()
14954 },
14955 ])))
14956 },
14957 );
14958
14959 editor.update_in(cx, |editor, window, cx| {
14960 cx.focus_self(window);
14961 editor.move_to_end(&MoveToEnd, window, cx);
14962 editor.handle_input(".", window, cx);
14963 });
14964 cx.run_until_parked();
14965 completion_handle.next().await.unwrap();
14966
14967 editor.update(cx, |editor, _| {
14968 assert!(editor.context_menu_visible());
14969 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14970 {
14971 let completion_labels = menu
14972 .completions
14973 .borrow()
14974 .iter()
14975 .map(|c| c.label.text.clone())
14976 .collect::<Vec<_>>();
14977 assert_eq!(
14978 completion_labels,
14979 &[
14980 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14981 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14982 "single line label 2 d e f ",
14983 "a b c g h i ",
14984 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14985 ],
14986 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14987 );
14988
14989 for completion in menu
14990 .completions
14991 .borrow()
14992 .iter() {
14993 assert_eq!(
14994 completion.label.filter_range,
14995 0..completion.label.text.len(),
14996 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14997 );
14998 }
14999 } else {
15000 panic!("expected completion menu to be open");
15001 }
15002 });
15003}
15004
15005#[gpui::test]
15006async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15007 init_test(cx, |_| {});
15008 let mut cx = EditorLspTestContext::new_rust(
15009 lsp::ServerCapabilities {
15010 completion_provider: Some(lsp::CompletionOptions {
15011 trigger_characters: Some(vec![".".to_string()]),
15012 ..Default::default()
15013 }),
15014 ..Default::default()
15015 },
15016 cx,
15017 )
15018 .await;
15019 cx.lsp
15020 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15021 Ok(Some(lsp::CompletionResponse::Array(vec![
15022 lsp::CompletionItem {
15023 label: "first".into(),
15024 ..Default::default()
15025 },
15026 lsp::CompletionItem {
15027 label: "last".into(),
15028 ..Default::default()
15029 },
15030 ])))
15031 });
15032 cx.set_state("variableˇ");
15033 cx.simulate_keystroke(".");
15034 cx.executor().run_until_parked();
15035
15036 cx.update_editor(|editor, _, _| {
15037 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15038 {
15039 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15040 } else {
15041 panic!("expected completion menu to be open");
15042 }
15043 });
15044
15045 cx.update_editor(|editor, window, cx| {
15046 editor.move_page_down(&MovePageDown::default(), window, cx);
15047 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15048 {
15049 assert!(
15050 menu.selected_item == 1,
15051 "expected PageDown to select the last item from the context menu"
15052 );
15053 } else {
15054 panic!("expected completion menu to stay open after PageDown");
15055 }
15056 });
15057
15058 cx.update_editor(|editor, window, cx| {
15059 editor.move_page_up(&MovePageUp::default(), window, cx);
15060 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15061 {
15062 assert!(
15063 menu.selected_item == 0,
15064 "expected PageUp to select the first item from the context menu"
15065 );
15066 } else {
15067 panic!("expected completion menu to stay open after PageUp");
15068 }
15069 });
15070}
15071
15072#[gpui::test]
15073async fn test_as_is_completions(cx: &mut TestAppContext) {
15074 init_test(cx, |_| {});
15075 let mut cx = EditorLspTestContext::new_rust(
15076 lsp::ServerCapabilities {
15077 completion_provider: Some(lsp::CompletionOptions {
15078 ..Default::default()
15079 }),
15080 ..Default::default()
15081 },
15082 cx,
15083 )
15084 .await;
15085 cx.lsp
15086 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15087 Ok(Some(lsp::CompletionResponse::Array(vec![
15088 lsp::CompletionItem {
15089 label: "unsafe".into(),
15090 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15091 range: lsp::Range {
15092 start: lsp::Position {
15093 line: 1,
15094 character: 2,
15095 },
15096 end: lsp::Position {
15097 line: 1,
15098 character: 3,
15099 },
15100 },
15101 new_text: "unsafe".to_string(),
15102 })),
15103 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15104 ..Default::default()
15105 },
15106 ])))
15107 });
15108 cx.set_state("fn a() {}\n nˇ");
15109 cx.executor().run_until_parked();
15110 cx.update_editor(|editor, window, cx| {
15111 editor.show_completions(
15112 &ShowCompletions {
15113 trigger: Some("\n".into()),
15114 },
15115 window,
15116 cx,
15117 );
15118 });
15119 cx.executor().run_until_parked();
15120
15121 cx.update_editor(|editor, window, cx| {
15122 editor.confirm_completion(&Default::default(), window, cx)
15123 });
15124 cx.executor().run_until_parked();
15125 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15126}
15127
15128#[gpui::test]
15129async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15130 init_test(cx, |_| {});
15131 let language =
15132 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15133 let mut cx = EditorLspTestContext::new(
15134 language,
15135 lsp::ServerCapabilities {
15136 completion_provider: Some(lsp::CompletionOptions {
15137 ..lsp::CompletionOptions::default()
15138 }),
15139 ..lsp::ServerCapabilities::default()
15140 },
15141 cx,
15142 )
15143 .await;
15144
15145 cx.set_state(
15146 "#ifndef BAR_H
15147#define BAR_H
15148
15149#include <stdbool.h>
15150
15151int fn_branch(bool do_branch1, bool do_branch2);
15152
15153#endif // BAR_H
15154ˇ",
15155 );
15156 cx.executor().run_until_parked();
15157 cx.update_editor(|editor, window, cx| {
15158 editor.handle_input("#", window, cx);
15159 });
15160 cx.executor().run_until_parked();
15161 cx.update_editor(|editor, window, cx| {
15162 editor.handle_input("i", window, cx);
15163 });
15164 cx.executor().run_until_parked();
15165 cx.update_editor(|editor, window, cx| {
15166 editor.handle_input("n", window, cx);
15167 });
15168 cx.executor().run_until_parked();
15169 cx.assert_editor_state(
15170 "#ifndef BAR_H
15171#define BAR_H
15172
15173#include <stdbool.h>
15174
15175int fn_branch(bool do_branch1, bool do_branch2);
15176
15177#endif // BAR_H
15178#inˇ",
15179 );
15180
15181 cx.lsp
15182 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15183 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15184 is_incomplete: false,
15185 item_defaults: None,
15186 items: vec![lsp::CompletionItem {
15187 kind: Some(lsp::CompletionItemKind::SNIPPET),
15188 label_details: Some(lsp::CompletionItemLabelDetails {
15189 detail: Some("header".to_string()),
15190 description: None,
15191 }),
15192 label: " include".to_string(),
15193 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15194 range: lsp::Range {
15195 start: lsp::Position {
15196 line: 8,
15197 character: 1,
15198 },
15199 end: lsp::Position {
15200 line: 8,
15201 character: 1,
15202 },
15203 },
15204 new_text: "include \"$0\"".to_string(),
15205 })),
15206 sort_text: Some("40b67681include".to_string()),
15207 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15208 filter_text: Some("include".to_string()),
15209 insert_text: Some("include \"$0\"".to_string()),
15210 ..lsp::CompletionItem::default()
15211 }],
15212 })))
15213 });
15214 cx.update_editor(|editor, window, cx| {
15215 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15216 });
15217 cx.executor().run_until_parked();
15218 cx.update_editor(|editor, window, cx| {
15219 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15220 });
15221 cx.executor().run_until_parked();
15222 cx.assert_editor_state(
15223 "#ifndef BAR_H
15224#define BAR_H
15225
15226#include <stdbool.h>
15227
15228int fn_branch(bool do_branch1, bool do_branch2);
15229
15230#endif // BAR_H
15231#include \"ˇ\"",
15232 );
15233
15234 cx.lsp
15235 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15236 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15237 is_incomplete: true,
15238 item_defaults: None,
15239 items: vec![lsp::CompletionItem {
15240 kind: Some(lsp::CompletionItemKind::FILE),
15241 label: "AGL/".to_string(),
15242 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15243 range: lsp::Range {
15244 start: lsp::Position {
15245 line: 8,
15246 character: 10,
15247 },
15248 end: lsp::Position {
15249 line: 8,
15250 character: 11,
15251 },
15252 },
15253 new_text: "AGL/".to_string(),
15254 })),
15255 sort_text: Some("40b67681AGL/".to_string()),
15256 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15257 filter_text: Some("AGL/".to_string()),
15258 insert_text: Some("AGL/".to_string()),
15259 ..lsp::CompletionItem::default()
15260 }],
15261 })))
15262 });
15263 cx.update_editor(|editor, window, cx| {
15264 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15265 });
15266 cx.executor().run_until_parked();
15267 cx.update_editor(|editor, window, cx| {
15268 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15269 });
15270 cx.executor().run_until_parked();
15271 cx.assert_editor_state(
15272 r##"#ifndef BAR_H
15273#define BAR_H
15274
15275#include <stdbool.h>
15276
15277int fn_branch(bool do_branch1, bool do_branch2);
15278
15279#endif // BAR_H
15280#include "AGL/ˇ"##,
15281 );
15282
15283 cx.update_editor(|editor, window, cx| {
15284 editor.handle_input("\"", window, cx);
15285 });
15286 cx.executor().run_until_parked();
15287 cx.assert_editor_state(
15288 r##"#ifndef BAR_H
15289#define BAR_H
15290
15291#include <stdbool.h>
15292
15293int fn_branch(bool do_branch1, bool do_branch2);
15294
15295#endif // BAR_H
15296#include "AGL/"ˇ"##,
15297 );
15298}
15299
15300#[gpui::test]
15301async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15302 init_test(cx, |_| {});
15303
15304 let mut cx = EditorLspTestContext::new_rust(
15305 lsp::ServerCapabilities {
15306 completion_provider: Some(lsp::CompletionOptions {
15307 trigger_characters: Some(vec![".".to_string()]),
15308 resolve_provider: Some(true),
15309 ..Default::default()
15310 }),
15311 ..Default::default()
15312 },
15313 cx,
15314 )
15315 .await;
15316
15317 cx.set_state("fn main() { let a = 2ˇ; }");
15318 cx.simulate_keystroke(".");
15319 let completion_item = lsp::CompletionItem {
15320 label: "Some".into(),
15321 kind: Some(lsp::CompletionItemKind::SNIPPET),
15322 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15323 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15324 kind: lsp::MarkupKind::Markdown,
15325 value: "```rust\nSome(2)\n```".to_string(),
15326 })),
15327 deprecated: Some(false),
15328 sort_text: Some("Some".to_string()),
15329 filter_text: Some("Some".to_string()),
15330 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15331 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15332 range: lsp::Range {
15333 start: lsp::Position {
15334 line: 0,
15335 character: 22,
15336 },
15337 end: lsp::Position {
15338 line: 0,
15339 character: 22,
15340 },
15341 },
15342 new_text: "Some(2)".to_string(),
15343 })),
15344 additional_text_edits: Some(vec![lsp::TextEdit {
15345 range: lsp::Range {
15346 start: lsp::Position {
15347 line: 0,
15348 character: 20,
15349 },
15350 end: lsp::Position {
15351 line: 0,
15352 character: 22,
15353 },
15354 },
15355 new_text: "".to_string(),
15356 }]),
15357 ..Default::default()
15358 };
15359
15360 let closure_completion_item = completion_item.clone();
15361 let counter = Arc::new(AtomicUsize::new(0));
15362 let counter_clone = counter.clone();
15363 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15364 let task_completion_item = closure_completion_item.clone();
15365 counter_clone.fetch_add(1, atomic::Ordering::Release);
15366 async move {
15367 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15368 is_incomplete: true,
15369 item_defaults: None,
15370 items: vec![task_completion_item],
15371 })))
15372 }
15373 });
15374
15375 cx.condition(|editor, _| editor.context_menu_visible())
15376 .await;
15377 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15378 assert!(request.next().await.is_some());
15379 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15380
15381 cx.simulate_keystrokes("S o m");
15382 cx.condition(|editor, _| editor.context_menu_visible())
15383 .await;
15384 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15385 assert!(request.next().await.is_some());
15386 assert!(request.next().await.is_some());
15387 assert!(request.next().await.is_some());
15388 request.close();
15389 assert!(request.next().await.is_none());
15390 assert_eq!(
15391 counter.load(atomic::Ordering::Acquire),
15392 4,
15393 "With the completions menu open, only one LSP request should happen per input"
15394 );
15395}
15396
15397#[gpui::test]
15398async fn test_toggle_comment(cx: &mut TestAppContext) {
15399 init_test(cx, |_| {});
15400 let mut cx = EditorTestContext::new(cx).await;
15401 let language = Arc::new(Language::new(
15402 LanguageConfig {
15403 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15404 ..Default::default()
15405 },
15406 Some(tree_sitter_rust::LANGUAGE.into()),
15407 ));
15408 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15409
15410 // If multiple selections intersect a line, the line is only toggled once.
15411 cx.set_state(indoc! {"
15412 fn a() {
15413 «//b();
15414 ˇ»// «c();
15415 //ˇ» d();
15416 }
15417 "});
15418
15419 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15420
15421 cx.assert_editor_state(indoc! {"
15422 fn a() {
15423 «b();
15424 c();
15425 ˇ» d();
15426 }
15427 "});
15428
15429 // The comment prefix is inserted at the same column for every line in a
15430 // selection.
15431 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15432
15433 cx.assert_editor_state(indoc! {"
15434 fn a() {
15435 // «b();
15436 // c();
15437 ˇ»// d();
15438 }
15439 "});
15440
15441 // If a selection ends at the beginning of a line, that line is not toggled.
15442 cx.set_selections_state(indoc! {"
15443 fn a() {
15444 // b();
15445 «// c();
15446 ˇ» // d();
15447 }
15448 "});
15449
15450 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15451
15452 cx.assert_editor_state(indoc! {"
15453 fn a() {
15454 // b();
15455 «c();
15456 ˇ» // d();
15457 }
15458 "});
15459
15460 // If a selection span a single line and is empty, the line is toggled.
15461 cx.set_state(indoc! {"
15462 fn a() {
15463 a();
15464 b();
15465 ˇ
15466 }
15467 "});
15468
15469 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15470
15471 cx.assert_editor_state(indoc! {"
15472 fn a() {
15473 a();
15474 b();
15475 //•ˇ
15476 }
15477 "});
15478
15479 // If a selection span multiple lines, empty lines are not toggled.
15480 cx.set_state(indoc! {"
15481 fn a() {
15482 «a();
15483
15484 c();ˇ»
15485 }
15486 "});
15487
15488 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15489
15490 cx.assert_editor_state(indoc! {"
15491 fn a() {
15492 // «a();
15493
15494 // c();ˇ»
15495 }
15496 "});
15497
15498 // If a selection includes multiple comment prefixes, all lines are uncommented.
15499 cx.set_state(indoc! {"
15500 fn a() {
15501 «// a();
15502 /// b();
15503 //! c();ˇ»
15504 }
15505 "});
15506
15507 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15508
15509 cx.assert_editor_state(indoc! {"
15510 fn a() {
15511 «a();
15512 b();
15513 c();ˇ»
15514 }
15515 "});
15516}
15517
15518#[gpui::test]
15519async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15520 init_test(cx, |_| {});
15521 let mut cx = EditorTestContext::new(cx).await;
15522 let language = Arc::new(Language::new(
15523 LanguageConfig {
15524 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15525 ..Default::default()
15526 },
15527 Some(tree_sitter_rust::LANGUAGE.into()),
15528 ));
15529 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15530
15531 let toggle_comments = &ToggleComments {
15532 advance_downwards: false,
15533 ignore_indent: true,
15534 };
15535
15536 // If multiple selections intersect a line, the line is only toggled once.
15537 cx.set_state(indoc! {"
15538 fn a() {
15539 // «b();
15540 // c();
15541 // ˇ» d();
15542 }
15543 "});
15544
15545 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15546
15547 cx.assert_editor_state(indoc! {"
15548 fn a() {
15549 «b();
15550 c();
15551 ˇ» d();
15552 }
15553 "});
15554
15555 // The comment prefix is inserted at the beginning of each line
15556 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15557
15558 cx.assert_editor_state(indoc! {"
15559 fn a() {
15560 // «b();
15561 // c();
15562 // ˇ» d();
15563 }
15564 "});
15565
15566 // If a selection ends at the beginning of a line, that line is not toggled.
15567 cx.set_selections_state(indoc! {"
15568 fn a() {
15569 // b();
15570 // «c();
15571 ˇ»// d();
15572 }
15573 "});
15574
15575 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15576
15577 cx.assert_editor_state(indoc! {"
15578 fn a() {
15579 // b();
15580 «c();
15581 ˇ»// d();
15582 }
15583 "});
15584
15585 // If a selection span a single line and is empty, the line is toggled.
15586 cx.set_state(indoc! {"
15587 fn a() {
15588 a();
15589 b();
15590 ˇ
15591 }
15592 "});
15593
15594 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15595
15596 cx.assert_editor_state(indoc! {"
15597 fn a() {
15598 a();
15599 b();
15600 //ˇ
15601 }
15602 "});
15603
15604 // If a selection span multiple lines, empty lines are not toggled.
15605 cx.set_state(indoc! {"
15606 fn a() {
15607 «a();
15608
15609 c();ˇ»
15610 }
15611 "});
15612
15613 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15614
15615 cx.assert_editor_state(indoc! {"
15616 fn a() {
15617 // «a();
15618
15619 // c();ˇ»
15620 }
15621 "});
15622
15623 // If a selection includes multiple comment prefixes, all lines are uncommented.
15624 cx.set_state(indoc! {"
15625 fn a() {
15626 // «a();
15627 /// b();
15628 //! c();ˇ»
15629 }
15630 "});
15631
15632 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15633
15634 cx.assert_editor_state(indoc! {"
15635 fn a() {
15636 «a();
15637 b();
15638 c();ˇ»
15639 }
15640 "});
15641}
15642
15643#[gpui::test]
15644async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15645 init_test(cx, |_| {});
15646
15647 let language = Arc::new(Language::new(
15648 LanguageConfig {
15649 line_comments: vec!["// ".into()],
15650 ..Default::default()
15651 },
15652 Some(tree_sitter_rust::LANGUAGE.into()),
15653 ));
15654
15655 let mut cx = EditorTestContext::new(cx).await;
15656
15657 cx.language_registry().add(language.clone());
15658 cx.update_buffer(|buffer, cx| {
15659 buffer.set_language(Some(language), cx);
15660 });
15661
15662 let toggle_comments = &ToggleComments {
15663 advance_downwards: true,
15664 ignore_indent: false,
15665 };
15666
15667 // Single cursor on one line -> advance
15668 // Cursor moves horizontally 3 characters as well on non-blank line
15669 cx.set_state(indoc!(
15670 "fn a() {
15671 ˇdog();
15672 cat();
15673 }"
15674 ));
15675 cx.update_editor(|editor, window, cx| {
15676 editor.toggle_comments(toggle_comments, window, cx);
15677 });
15678 cx.assert_editor_state(indoc!(
15679 "fn a() {
15680 // dog();
15681 catˇ();
15682 }"
15683 ));
15684
15685 // Single selection on one line -> don't advance
15686 cx.set_state(indoc!(
15687 "fn a() {
15688 «dog()ˇ»;
15689 cat();
15690 }"
15691 ));
15692 cx.update_editor(|editor, window, cx| {
15693 editor.toggle_comments(toggle_comments, window, cx);
15694 });
15695 cx.assert_editor_state(indoc!(
15696 "fn a() {
15697 // «dog()ˇ»;
15698 cat();
15699 }"
15700 ));
15701
15702 // Multiple cursors on one line -> advance
15703 cx.set_state(indoc!(
15704 "fn a() {
15705 ˇdˇog();
15706 cat();
15707 }"
15708 ));
15709 cx.update_editor(|editor, window, cx| {
15710 editor.toggle_comments(toggle_comments, window, cx);
15711 });
15712 cx.assert_editor_state(indoc!(
15713 "fn a() {
15714 // dog();
15715 catˇ(ˇ);
15716 }"
15717 ));
15718
15719 // Multiple cursors on one line, with selection -> don't advance
15720 cx.set_state(indoc!(
15721 "fn a() {
15722 ˇdˇog«()ˇ»;
15723 cat();
15724 }"
15725 ));
15726 cx.update_editor(|editor, window, cx| {
15727 editor.toggle_comments(toggle_comments, window, cx);
15728 });
15729 cx.assert_editor_state(indoc!(
15730 "fn a() {
15731 // ˇdˇog«()ˇ»;
15732 cat();
15733 }"
15734 ));
15735
15736 // Single cursor on one line -> advance
15737 // Cursor moves to column 0 on blank line
15738 cx.set_state(indoc!(
15739 "fn a() {
15740 ˇdog();
15741
15742 cat();
15743 }"
15744 ));
15745 cx.update_editor(|editor, window, cx| {
15746 editor.toggle_comments(toggle_comments, window, cx);
15747 });
15748 cx.assert_editor_state(indoc!(
15749 "fn a() {
15750 // dog();
15751 ˇ
15752 cat();
15753 }"
15754 ));
15755
15756 // Single cursor on one line -> advance
15757 // Cursor starts and ends at column 0
15758 cx.set_state(indoc!(
15759 "fn a() {
15760 ˇ dog();
15761 cat();
15762 }"
15763 ));
15764 cx.update_editor(|editor, window, cx| {
15765 editor.toggle_comments(toggle_comments, window, cx);
15766 });
15767 cx.assert_editor_state(indoc!(
15768 "fn a() {
15769 // dog();
15770 ˇ cat();
15771 }"
15772 ));
15773}
15774
15775#[gpui::test]
15776async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15777 init_test(cx, |_| {});
15778
15779 let mut cx = EditorTestContext::new(cx).await;
15780
15781 let html_language = Arc::new(
15782 Language::new(
15783 LanguageConfig {
15784 name: "HTML".into(),
15785 block_comment: Some(BlockCommentConfig {
15786 start: "<!-- ".into(),
15787 prefix: "".into(),
15788 end: " -->".into(),
15789 tab_size: 0,
15790 }),
15791 ..Default::default()
15792 },
15793 Some(tree_sitter_html::LANGUAGE.into()),
15794 )
15795 .with_injection_query(
15796 r#"
15797 (script_element
15798 (raw_text) @injection.content
15799 (#set! injection.language "javascript"))
15800 "#,
15801 )
15802 .unwrap(),
15803 );
15804
15805 let javascript_language = Arc::new(Language::new(
15806 LanguageConfig {
15807 name: "JavaScript".into(),
15808 line_comments: vec!["// ".into()],
15809 ..Default::default()
15810 },
15811 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15812 ));
15813
15814 cx.language_registry().add(html_language.clone());
15815 cx.language_registry().add(javascript_language);
15816 cx.update_buffer(|buffer, cx| {
15817 buffer.set_language(Some(html_language), cx);
15818 });
15819
15820 // Toggle comments for empty selections
15821 cx.set_state(
15822 &r#"
15823 <p>A</p>ˇ
15824 <p>B</p>ˇ
15825 <p>C</p>ˇ
15826 "#
15827 .unindent(),
15828 );
15829 cx.update_editor(|editor, window, cx| {
15830 editor.toggle_comments(&ToggleComments::default(), window, cx)
15831 });
15832 cx.assert_editor_state(
15833 &r#"
15834 <!-- <p>A</p>ˇ -->
15835 <!-- <p>B</p>ˇ -->
15836 <!-- <p>C</p>ˇ -->
15837 "#
15838 .unindent(),
15839 );
15840 cx.update_editor(|editor, window, cx| {
15841 editor.toggle_comments(&ToggleComments::default(), window, cx)
15842 });
15843 cx.assert_editor_state(
15844 &r#"
15845 <p>A</p>ˇ
15846 <p>B</p>ˇ
15847 <p>C</p>ˇ
15848 "#
15849 .unindent(),
15850 );
15851
15852 // Toggle comments for mixture of empty and non-empty selections, where
15853 // multiple selections occupy a given line.
15854 cx.set_state(
15855 &r#"
15856 <p>A«</p>
15857 <p>ˇ»B</p>ˇ
15858 <p>C«</p>
15859 <p>ˇ»D</p>ˇ
15860 "#
15861 .unindent(),
15862 );
15863
15864 cx.update_editor(|editor, window, cx| {
15865 editor.toggle_comments(&ToggleComments::default(), window, cx)
15866 });
15867 cx.assert_editor_state(
15868 &r#"
15869 <!-- <p>A«</p>
15870 <p>ˇ»B</p>ˇ -->
15871 <!-- <p>C«</p>
15872 <p>ˇ»D</p>ˇ -->
15873 "#
15874 .unindent(),
15875 );
15876 cx.update_editor(|editor, window, cx| {
15877 editor.toggle_comments(&ToggleComments::default(), window, cx)
15878 });
15879 cx.assert_editor_state(
15880 &r#"
15881 <p>A«</p>
15882 <p>ˇ»B</p>ˇ
15883 <p>C«</p>
15884 <p>ˇ»D</p>ˇ
15885 "#
15886 .unindent(),
15887 );
15888
15889 // Toggle comments when different languages are active for different
15890 // selections.
15891 cx.set_state(
15892 &r#"
15893 ˇ<script>
15894 ˇvar x = new Y();
15895 ˇ</script>
15896 "#
15897 .unindent(),
15898 );
15899 cx.executor().run_until_parked();
15900 cx.update_editor(|editor, window, cx| {
15901 editor.toggle_comments(&ToggleComments::default(), window, cx)
15902 });
15903 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15904 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15905 cx.assert_editor_state(
15906 &r#"
15907 <!-- ˇ<script> -->
15908 // ˇvar x = new Y();
15909 <!-- ˇ</script> -->
15910 "#
15911 .unindent(),
15912 );
15913}
15914
15915#[gpui::test]
15916fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15917 init_test(cx, |_| {});
15918
15919 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15920 let multibuffer = cx.new(|cx| {
15921 let mut multibuffer = MultiBuffer::new(ReadWrite);
15922 multibuffer.push_excerpts(
15923 buffer.clone(),
15924 [
15925 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15926 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15927 ],
15928 cx,
15929 );
15930 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15931 multibuffer
15932 });
15933
15934 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15935 editor.update_in(cx, |editor, window, cx| {
15936 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15937 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15938 s.select_ranges([
15939 Point::new(0, 0)..Point::new(0, 0),
15940 Point::new(1, 0)..Point::new(1, 0),
15941 ])
15942 });
15943
15944 editor.handle_input("X", window, cx);
15945 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15946 assert_eq!(
15947 editor.selections.ranges(cx),
15948 [
15949 Point::new(0, 1)..Point::new(0, 1),
15950 Point::new(1, 1)..Point::new(1, 1),
15951 ]
15952 );
15953
15954 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15955 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15956 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15957 });
15958 editor.backspace(&Default::default(), window, cx);
15959 assert_eq!(editor.text(cx), "Xa\nbbb");
15960 assert_eq!(
15961 editor.selections.ranges(cx),
15962 [Point::new(1, 0)..Point::new(1, 0)]
15963 );
15964
15965 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15966 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15967 });
15968 editor.backspace(&Default::default(), window, cx);
15969 assert_eq!(editor.text(cx), "X\nbb");
15970 assert_eq!(
15971 editor.selections.ranges(cx),
15972 [Point::new(0, 1)..Point::new(0, 1)]
15973 );
15974 });
15975}
15976
15977#[gpui::test]
15978fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15979 init_test(cx, |_| {});
15980
15981 let markers = vec![('[', ']').into(), ('(', ')').into()];
15982 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15983 indoc! {"
15984 [aaaa
15985 (bbbb]
15986 cccc)",
15987 },
15988 markers.clone(),
15989 );
15990 let excerpt_ranges = markers.into_iter().map(|marker| {
15991 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15992 ExcerptRange::new(context)
15993 });
15994 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15995 let multibuffer = cx.new(|cx| {
15996 let mut multibuffer = MultiBuffer::new(ReadWrite);
15997 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15998 multibuffer
15999 });
16000
16001 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16002 editor.update_in(cx, |editor, window, cx| {
16003 let (expected_text, selection_ranges) = marked_text_ranges(
16004 indoc! {"
16005 aaaa
16006 bˇbbb
16007 bˇbbˇb
16008 cccc"
16009 },
16010 true,
16011 );
16012 assert_eq!(editor.text(cx), expected_text);
16013 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16014 s.select_ranges(selection_ranges)
16015 });
16016
16017 editor.handle_input("X", window, cx);
16018
16019 let (expected_text, expected_selections) = marked_text_ranges(
16020 indoc! {"
16021 aaaa
16022 bXˇbbXb
16023 bXˇbbXˇb
16024 cccc"
16025 },
16026 false,
16027 );
16028 assert_eq!(editor.text(cx), expected_text);
16029 assert_eq!(editor.selections.ranges(cx), expected_selections);
16030
16031 editor.newline(&Newline, window, cx);
16032 let (expected_text, expected_selections) = marked_text_ranges(
16033 indoc! {"
16034 aaaa
16035 bX
16036 ˇbbX
16037 b
16038 bX
16039 ˇbbX
16040 ˇb
16041 cccc"
16042 },
16043 false,
16044 );
16045 assert_eq!(editor.text(cx), expected_text);
16046 assert_eq!(editor.selections.ranges(cx), expected_selections);
16047 });
16048}
16049
16050#[gpui::test]
16051fn test_refresh_selections(cx: &mut TestAppContext) {
16052 init_test(cx, |_| {});
16053
16054 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16055 let mut excerpt1_id = None;
16056 let multibuffer = cx.new(|cx| {
16057 let mut multibuffer = MultiBuffer::new(ReadWrite);
16058 excerpt1_id = multibuffer
16059 .push_excerpts(
16060 buffer.clone(),
16061 [
16062 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16063 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16064 ],
16065 cx,
16066 )
16067 .into_iter()
16068 .next();
16069 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16070 multibuffer
16071 });
16072
16073 let editor = cx.add_window(|window, cx| {
16074 let mut editor = build_editor(multibuffer.clone(), window, cx);
16075 let snapshot = editor.snapshot(window, cx);
16076 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16077 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16078 });
16079 editor.begin_selection(
16080 Point::new(2, 1).to_display_point(&snapshot),
16081 true,
16082 1,
16083 window,
16084 cx,
16085 );
16086 assert_eq!(
16087 editor.selections.ranges(cx),
16088 [
16089 Point::new(1, 3)..Point::new(1, 3),
16090 Point::new(2, 1)..Point::new(2, 1),
16091 ]
16092 );
16093 editor
16094 });
16095
16096 // Refreshing selections is a no-op when excerpts haven't changed.
16097 _ = editor.update(cx, |editor, window, cx| {
16098 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16099 assert_eq!(
16100 editor.selections.ranges(cx),
16101 [
16102 Point::new(1, 3)..Point::new(1, 3),
16103 Point::new(2, 1)..Point::new(2, 1),
16104 ]
16105 );
16106 });
16107
16108 multibuffer.update(cx, |multibuffer, cx| {
16109 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16110 });
16111 _ = editor.update(cx, |editor, window, cx| {
16112 // Removing an excerpt causes the first selection to become degenerate.
16113 assert_eq!(
16114 editor.selections.ranges(cx),
16115 [
16116 Point::new(0, 0)..Point::new(0, 0),
16117 Point::new(0, 1)..Point::new(0, 1)
16118 ]
16119 );
16120
16121 // Refreshing selections will relocate the first selection to the original buffer
16122 // location.
16123 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16124 assert_eq!(
16125 editor.selections.ranges(cx),
16126 [
16127 Point::new(0, 1)..Point::new(0, 1),
16128 Point::new(0, 3)..Point::new(0, 3)
16129 ]
16130 );
16131 assert!(editor.selections.pending_anchor().is_some());
16132 });
16133}
16134
16135#[gpui::test]
16136fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16137 init_test(cx, |_| {});
16138
16139 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16140 let mut excerpt1_id = None;
16141 let multibuffer = cx.new(|cx| {
16142 let mut multibuffer = MultiBuffer::new(ReadWrite);
16143 excerpt1_id = multibuffer
16144 .push_excerpts(
16145 buffer.clone(),
16146 [
16147 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16148 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16149 ],
16150 cx,
16151 )
16152 .into_iter()
16153 .next();
16154 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16155 multibuffer
16156 });
16157
16158 let editor = cx.add_window(|window, cx| {
16159 let mut editor = build_editor(multibuffer.clone(), window, cx);
16160 let snapshot = editor.snapshot(window, cx);
16161 editor.begin_selection(
16162 Point::new(1, 3).to_display_point(&snapshot),
16163 false,
16164 1,
16165 window,
16166 cx,
16167 );
16168 assert_eq!(
16169 editor.selections.ranges(cx),
16170 [Point::new(1, 3)..Point::new(1, 3)]
16171 );
16172 editor
16173 });
16174
16175 multibuffer.update(cx, |multibuffer, cx| {
16176 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16177 });
16178 _ = editor.update(cx, |editor, window, cx| {
16179 assert_eq!(
16180 editor.selections.ranges(cx),
16181 [Point::new(0, 0)..Point::new(0, 0)]
16182 );
16183
16184 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16185 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16186 assert_eq!(
16187 editor.selections.ranges(cx),
16188 [Point::new(0, 3)..Point::new(0, 3)]
16189 );
16190 assert!(editor.selections.pending_anchor().is_some());
16191 });
16192}
16193
16194#[gpui::test]
16195async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16196 init_test(cx, |_| {});
16197
16198 let language = Arc::new(
16199 Language::new(
16200 LanguageConfig {
16201 brackets: BracketPairConfig {
16202 pairs: vec![
16203 BracketPair {
16204 start: "{".to_string(),
16205 end: "}".to_string(),
16206 close: true,
16207 surround: true,
16208 newline: true,
16209 },
16210 BracketPair {
16211 start: "/* ".to_string(),
16212 end: " */".to_string(),
16213 close: true,
16214 surround: true,
16215 newline: true,
16216 },
16217 ],
16218 ..Default::default()
16219 },
16220 ..Default::default()
16221 },
16222 Some(tree_sitter_rust::LANGUAGE.into()),
16223 )
16224 .with_indents_query("")
16225 .unwrap(),
16226 );
16227
16228 let text = concat!(
16229 "{ }\n", //
16230 " x\n", //
16231 " /* */\n", //
16232 "x\n", //
16233 "{{} }\n", //
16234 );
16235
16236 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16237 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16238 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16239 editor
16240 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16241 .await;
16242
16243 editor.update_in(cx, |editor, window, cx| {
16244 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16245 s.select_display_ranges([
16246 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16247 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16248 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16249 ])
16250 });
16251 editor.newline(&Newline, window, cx);
16252
16253 assert_eq!(
16254 editor.buffer().read(cx).read(cx).text(),
16255 concat!(
16256 "{ \n", // Suppress rustfmt
16257 "\n", //
16258 "}\n", //
16259 " x\n", //
16260 " /* \n", //
16261 " \n", //
16262 " */\n", //
16263 "x\n", //
16264 "{{} \n", //
16265 "}\n", //
16266 )
16267 );
16268 });
16269}
16270
16271#[gpui::test]
16272fn test_highlighted_ranges(cx: &mut TestAppContext) {
16273 init_test(cx, |_| {});
16274
16275 let editor = cx.add_window(|window, cx| {
16276 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16277 build_editor(buffer, window, cx)
16278 });
16279
16280 _ = editor.update(cx, |editor, window, cx| {
16281 struct Type1;
16282 struct Type2;
16283
16284 let buffer = editor.buffer.read(cx).snapshot(cx);
16285
16286 let anchor_range =
16287 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16288
16289 editor.highlight_background::<Type1>(
16290 &[
16291 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16292 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16293 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16294 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16295 ],
16296 |_| Hsla::red(),
16297 cx,
16298 );
16299 editor.highlight_background::<Type2>(
16300 &[
16301 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16302 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16303 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16304 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16305 ],
16306 |_| Hsla::green(),
16307 cx,
16308 );
16309
16310 let snapshot = editor.snapshot(window, cx);
16311 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16312 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16313 &snapshot,
16314 cx.theme(),
16315 );
16316 assert_eq!(
16317 highlighted_ranges,
16318 &[
16319 (
16320 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16321 Hsla::green(),
16322 ),
16323 (
16324 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16325 Hsla::red(),
16326 ),
16327 (
16328 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16329 Hsla::green(),
16330 ),
16331 (
16332 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16333 Hsla::red(),
16334 ),
16335 ]
16336 );
16337 assert_eq!(
16338 editor.sorted_background_highlights_in_range(
16339 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16340 &snapshot,
16341 cx.theme(),
16342 ),
16343 &[(
16344 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16345 Hsla::red(),
16346 )]
16347 );
16348 });
16349}
16350
16351#[gpui::test]
16352async fn test_following(cx: &mut TestAppContext) {
16353 init_test(cx, |_| {});
16354
16355 let fs = FakeFs::new(cx.executor());
16356 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16357
16358 let buffer = project.update(cx, |project, cx| {
16359 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16360 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16361 });
16362 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16363 let follower = cx.update(|cx| {
16364 cx.open_window(
16365 WindowOptions {
16366 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16367 gpui::Point::new(px(0.), px(0.)),
16368 gpui::Point::new(px(10.), px(80.)),
16369 ))),
16370 ..Default::default()
16371 },
16372 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16373 )
16374 .unwrap()
16375 });
16376
16377 let is_still_following = Rc::new(RefCell::new(true));
16378 let follower_edit_event_count = Rc::new(RefCell::new(0));
16379 let pending_update = Rc::new(RefCell::new(None));
16380 let leader_entity = leader.root(cx).unwrap();
16381 let follower_entity = follower.root(cx).unwrap();
16382 _ = follower.update(cx, {
16383 let update = pending_update.clone();
16384 let is_still_following = is_still_following.clone();
16385 let follower_edit_event_count = follower_edit_event_count.clone();
16386 |_, window, cx| {
16387 cx.subscribe_in(
16388 &leader_entity,
16389 window,
16390 move |_, leader, event, window, cx| {
16391 leader.read(cx).add_event_to_update_proto(
16392 event,
16393 &mut update.borrow_mut(),
16394 window,
16395 cx,
16396 );
16397 },
16398 )
16399 .detach();
16400
16401 cx.subscribe_in(
16402 &follower_entity,
16403 window,
16404 move |_, _, event: &EditorEvent, _window, _cx| {
16405 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16406 *is_still_following.borrow_mut() = false;
16407 }
16408
16409 if let EditorEvent::BufferEdited = event {
16410 *follower_edit_event_count.borrow_mut() += 1;
16411 }
16412 },
16413 )
16414 .detach();
16415 }
16416 });
16417
16418 // Update the selections only
16419 _ = leader.update(cx, |leader, window, cx| {
16420 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16421 s.select_ranges([1..1])
16422 });
16423 });
16424 follower
16425 .update(cx, |follower, window, cx| {
16426 follower.apply_update_proto(
16427 &project,
16428 pending_update.borrow_mut().take().unwrap(),
16429 window,
16430 cx,
16431 )
16432 })
16433 .unwrap()
16434 .await
16435 .unwrap();
16436 _ = follower.update(cx, |follower, _, cx| {
16437 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
16438 });
16439 assert!(*is_still_following.borrow());
16440 assert_eq!(*follower_edit_event_count.borrow(), 0);
16441
16442 // Update the scroll position only
16443 _ = leader.update(cx, |leader, window, cx| {
16444 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16445 });
16446 follower
16447 .update(cx, |follower, window, cx| {
16448 follower.apply_update_proto(
16449 &project,
16450 pending_update.borrow_mut().take().unwrap(),
16451 window,
16452 cx,
16453 )
16454 })
16455 .unwrap()
16456 .await
16457 .unwrap();
16458 assert_eq!(
16459 follower
16460 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16461 .unwrap(),
16462 gpui::Point::new(1.5, 3.5)
16463 );
16464 assert!(*is_still_following.borrow());
16465 assert_eq!(*follower_edit_event_count.borrow(), 0);
16466
16467 // Update the selections and scroll position. The follower's scroll position is updated
16468 // via autoscroll, not via the leader's exact scroll position.
16469 _ = leader.update(cx, |leader, window, cx| {
16470 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16471 s.select_ranges([0..0])
16472 });
16473 leader.request_autoscroll(Autoscroll::newest(), cx);
16474 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16475 });
16476 follower
16477 .update(cx, |follower, window, cx| {
16478 follower.apply_update_proto(
16479 &project,
16480 pending_update.borrow_mut().take().unwrap(),
16481 window,
16482 cx,
16483 )
16484 })
16485 .unwrap()
16486 .await
16487 .unwrap();
16488 _ = follower.update(cx, |follower, _, cx| {
16489 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16490 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16491 });
16492 assert!(*is_still_following.borrow());
16493
16494 // Creating a pending selection that precedes another selection
16495 _ = leader.update(cx, |leader, window, cx| {
16496 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16497 s.select_ranges([1..1])
16498 });
16499 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16500 });
16501 follower
16502 .update(cx, |follower, window, cx| {
16503 follower.apply_update_proto(
16504 &project,
16505 pending_update.borrow_mut().take().unwrap(),
16506 window,
16507 cx,
16508 )
16509 })
16510 .unwrap()
16511 .await
16512 .unwrap();
16513 _ = follower.update(cx, |follower, _, cx| {
16514 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16515 });
16516 assert!(*is_still_following.borrow());
16517
16518 // Extend the pending selection so that it surrounds another selection
16519 _ = leader.update(cx, |leader, window, cx| {
16520 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16521 });
16522 follower
16523 .update(cx, |follower, window, cx| {
16524 follower.apply_update_proto(
16525 &project,
16526 pending_update.borrow_mut().take().unwrap(),
16527 window,
16528 cx,
16529 )
16530 })
16531 .unwrap()
16532 .await
16533 .unwrap();
16534 _ = follower.update(cx, |follower, _, cx| {
16535 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16536 });
16537
16538 // Scrolling locally breaks the follow
16539 _ = follower.update(cx, |follower, window, cx| {
16540 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16541 follower.set_scroll_anchor(
16542 ScrollAnchor {
16543 anchor: top_anchor,
16544 offset: gpui::Point::new(0.0, 0.5),
16545 },
16546 window,
16547 cx,
16548 );
16549 });
16550 assert!(!(*is_still_following.borrow()));
16551}
16552
16553#[gpui::test]
16554async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16555 init_test(cx, |_| {});
16556
16557 let fs = FakeFs::new(cx.executor());
16558 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16559 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16560 let pane = workspace
16561 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16562 .unwrap();
16563
16564 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16565
16566 let leader = pane.update_in(cx, |_, window, cx| {
16567 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16568 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16569 });
16570
16571 // Start following the editor when it has no excerpts.
16572 let mut state_message =
16573 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16574 let workspace_entity = workspace.root(cx).unwrap();
16575 let follower_1 = cx
16576 .update_window(*workspace.deref(), |_, window, cx| {
16577 Editor::from_state_proto(
16578 workspace_entity,
16579 ViewId {
16580 creator: CollaboratorId::PeerId(PeerId::default()),
16581 id: 0,
16582 },
16583 &mut state_message,
16584 window,
16585 cx,
16586 )
16587 })
16588 .unwrap()
16589 .unwrap()
16590 .await
16591 .unwrap();
16592
16593 let update_message = Rc::new(RefCell::new(None));
16594 follower_1.update_in(cx, {
16595 let update = update_message.clone();
16596 |_, window, cx| {
16597 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16598 leader.read(cx).add_event_to_update_proto(
16599 event,
16600 &mut update.borrow_mut(),
16601 window,
16602 cx,
16603 );
16604 })
16605 .detach();
16606 }
16607 });
16608
16609 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16610 (
16611 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16612 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16613 )
16614 });
16615
16616 // Insert some excerpts.
16617 leader.update(cx, |leader, cx| {
16618 leader.buffer.update(cx, |multibuffer, cx| {
16619 multibuffer.set_excerpts_for_path(
16620 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
16621 buffer_1.clone(),
16622 vec![
16623 Point::row_range(0..3),
16624 Point::row_range(1..6),
16625 Point::row_range(12..15),
16626 ],
16627 0,
16628 cx,
16629 );
16630 multibuffer.set_excerpts_for_path(
16631 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
16632 buffer_2.clone(),
16633 vec![Point::row_range(0..6), Point::row_range(8..12)],
16634 0,
16635 cx,
16636 );
16637 });
16638 });
16639
16640 // Apply the update of adding the excerpts.
16641 follower_1
16642 .update_in(cx, |follower, window, cx| {
16643 follower.apply_update_proto(
16644 &project,
16645 update_message.borrow().clone().unwrap(),
16646 window,
16647 cx,
16648 )
16649 })
16650 .await
16651 .unwrap();
16652 assert_eq!(
16653 follower_1.update(cx, |editor, cx| editor.text(cx)),
16654 leader.update(cx, |editor, cx| editor.text(cx))
16655 );
16656 update_message.borrow_mut().take();
16657
16658 // Start following separately after it already has excerpts.
16659 let mut state_message =
16660 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16661 let workspace_entity = workspace.root(cx).unwrap();
16662 let follower_2 = cx
16663 .update_window(*workspace.deref(), |_, window, cx| {
16664 Editor::from_state_proto(
16665 workspace_entity,
16666 ViewId {
16667 creator: CollaboratorId::PeerId(PeerId::default()),
16668 id: 0,
16669 },
16670 &mut state_message,
16671 window,
16672 cx,
16673 )
16674 })
16675 .unwrap()
16676 .unwrap()
16677 .await
16678 .unwrap();
16679 assert_eq!(
16680 follower_2.update(cx, |editor, cx| editor.text(cx)),
16681 leader.update(cx, |editor, cx| editor.text(cx))
16682 );
16683
16684 // Remove some excerpts.
16685 leader.update(cx, |leader, cx| {
16686 leader.buffer.update(cx, |multibuffer, cx| {
16687 let excerpt_ids = multibuffer.excerpt_ids();
16688 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16689 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16690 });
16691 });
16692
16693 // Apply the update of removing the excerpts.
16694 follower_1
16695 .update_in(cx, |follower, window, cx| {
16696 follower.apply_update_proto(
16697 &project,
16698 update_message.borrow().clone().unwrap(),
16699 window,
16700 cx,
16701 )
16702 })
16703 .await
16704 .unwrap();
16705 follower_2
16706 .update_in(cx, |follower, window, cx| {
16707 follower.apply_update_proto(
16708 &project,
16709 update_message.borrow().clone().unwrap(),
16710 window,
16711 cx,
16712 )
16713 })
16714 .await
16715 .unwrap();
16716 update_message.borrow_mut().take();
16717 assert_eq!(
16718 follower_1.update(cx, |editor, cx| editor.text(cx)),
16719 leader.update(cx, |editor, cx| editor.text(cx))
16720 );
16721}
16722
16723#[gpui::test]
16724async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16725 init_test(cx, |_| {});
16726
16727 let mut cx = EditorTestContext::new(cx).await;
16728 let lsp_store =
16729 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16730
16731 cx.set_state(indoc! {"
16732 ˇfn func(abc def: i32) -> u32 {
16733 }
16734 "});
16735
16736 cx.update(|_, cx| {
16737 lsp_store.update(cx, |lsp_store, cx| {
16738 lsp_store
16739 .update_diagnostics(
16740 LanguageServerId(0),
16741 lsp::PublishDiagnosticsParams {
16742 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16743 version: None,
16744 diagnostics: vec![
16745 lsp::Diagnostic {
16746 range: lsp::Range::new(
16747 lsp::Position::new(0, 11),
16748 lsp::Position::new(0, 12),
16749 ),
16750 severity: Some(lsp::DiagnosticSeverity::ERROR),
16751 ..Default::default()
16752 },
16753 lsp::Diagnostic {
16754 range: lsp::Range::new(
16755 lsp::Position::new(0, 12),
16756 lsp::Position::new(0, 15),
16757 ),
16758 severity: Some(lsp::DiagnosticSeverity::ERROR),
16759 ..Default::default()
16760 },
16761 lsp::Diagnostic {
16762 range: lsp::Range::new(
16763 lsp::Position::new(0, 25),
16764 lsp::Position::new(0, 28),
16765 ),
16766 severity: Some(lsp::DiagnosticSeverity::ERROR),
16767 ..Default::default()
16768 },
16769 ],
16770 },
16771 None,
16772 DiagnosticSourceKind::Pushed,
16773 &[],
16774 cx,
16775 )
16776 .unwrap()
16777 });
16778 });
16779
16780 executor.run_until_parked();
16781
16782 cx.update_editor(|editor, window, cx| {
16783 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16784 });
16785
16786 cx.assert_editor_state(indoc! {"
16787 fn func(abc def: i32) -> ˇu32 {
16788 }
16789 "});
16790
16791 cx.update_editor(|editor, window, cx| {
16792 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16793 });
16794
16795 cx.assert_editor_state(indoc! {"
16796 fn func(abc ˇdef: i32) -> u32 {
16797 }
16798 "});
16799
16800 cx.update_editor(|editor, window, cx| {
16801 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16802 });
16803
16804 cx.assert_editor_state(indoc! {"
16805 fn func(abcˇ def: i32) -> u32 {
16806 }
16807 "});
16808
16809 cx.update_editor(|editor, window, cx| {
16810 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16811 });
16812
16813 cx.assert_editor_state(indoc! {"
16814 fn func(abc def: i32) -> ˇu32 {
16815 }
16816 "});
16817}
16818
16819#[gpui::test]
16820async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16821 init_test(cx, |_| {});
16822
16823 let mut cx = EditorTestContext::new(cx).await;
16824
16825 let diff_base = r#"
16826 use some::mod;
16827
16828 const A: u32 = 42;
16829
16830 fn main() {
16831 println!("hello");
16832
16833 println!("world");
16834 }
16835 "#
16836 .unindent();
16837
16838 // Edits are modified, removed, modified, added
16839 cx.set_state(
16840 &r#"
16841 use some::modified;
16842
16843 ˇ
16844 fn main() {
16845 println!("hello there");
16846
16847 println!("around the");
16848 println!("world");
16849 }
16850 "#
16851 .unindent(),
16852 );
16853
16854 cx.set_head_text(&diff_base);
16855 executor.run_until_parked();
16856
16857 cx.update_editor(|editor, window, cx| {
16858 //Wrap around the bottom of the buffer
16859 for _ in 0..3 {
16860 editor.go_to_next_hunk(&GoToHunk, window, cx);
16861 }
16862 });
16863
16864 cx.assert_editor_state(
16865 &r#"
16866 ˇuse some::modified;
16867
16868
16869 fn main() {
16870 println!("hello there");
16871
16872 println!("around the");
16873 println!("world");
16874 }
16875 "#
16876 .unindent(),
16877 );
16878
16879 cx.update_editor(|editor, window, cx| {
16880 //Wrap around the top of the buffer
16881 for _ in 0..2 {
16882 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16883 }
16884 });
16885
16886 cx.assert_editor_state(
16887 &r#"
16888 use some::modified;
16889
16890
16891 fn main() {
16892 ˇ println!("hello there");
16893
16894 println!("around the");
16895 println!("world");
16896 }
16897 "#
16898 .unindent(),
16899 );
16900
16901 cx.update_editor(|editor, window, cx| {
16902 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16903 });
16904
16905 cx.assert_editor_state(
16906 &r#"
16907 use some::modified;
16908
16909 ˇ
16910 fn main() {
16911 println!("hello there");
16912
16913 println!("around the");
16914 println!("world");
16915 }
16916 "#
16917 .unindent(),
16918 );
16919
16920 cx.update_editor(|editor, window, cx| {
16921 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16922 });
16923
16924 cx.assert_editor_state(
16925 &r#"
16926 ˇuse some::modified;
16927
16928
16929 fn main() {
16930 println!("hello there");
16931
16932 println!("around the");
16933 println!("world");
16934 }
16935 "#
16936 .unindent(),
16937 );
16938
16939 cx.update_editor(|editor, window, cx| {
16940 for _ in 0..2 {
16941 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16942 }
16943 });
16944
16945 cx.assert_editor_state(
16946 &r#"
16947 use some::modified;
16948
16949
16950 fn main() {
16951 ˇ println!("hello there");
16952
16953 println!("around the");
16954 println!("world");
16955 }
16956 "#
16957 .unindent(),
16958 );
16959
16960 cx.update_editor(|editor, window, cx| {
16961 editor.fold(&Fold, window, cx);
16962 });
16963
16964 cx.update_editor(|editor, window, cx| {
16965 editor.go_to_next_hunk(&GoToHunk, window, cx);
16966 });
16967
16968 cx.assert_editor_state(
16969 &r#"
16970 ˇuse some::modified;
16971
16972
16973 fn main() {
16974 println!("hello there");
16975
16976 println!("around the");
16977 println!("world");
16978 }
16979 "#
16980 .unindent(),
16981 );
16982}
16983
16984#[test]
16985fn test_split_words() {
16986 fn split(text: &str) -> Vec<&str> {
16987 split_words(text).collect()
16988 }
16989
16990 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16991 assert_eq!(split("hello_world"), &["hello_", "world"]);
16992 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16993 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16994 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16995 assert_eq!(split("helloworld"), &["helloworld"]);
16996
16997 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16998}
16999
17000#[gpui::test]
17001async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17002 init_test(cx, |_| {});
17003
17004 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17005 let mut assert = |before, after| {
17006 let _state_context = cx.set_state(before);
17007 cx.run_until_parked();
17008 cx.update_editor(|editor, window, cx| {
17009 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17010 });
17011 cx.run_until_parked();
17012 cx.assert_editor_state(after);
17013 };
17014
17015 // Outside bracket jumps to outside of matching bracket
17016 assert("console.logˇ(var);", "console.log(var)ˇ;");
17017 assert("console.log(var)ˇ;", "console.logˇ(var);");
17018
17019 // Inside bracket jumps to inside of matching bracket
17020 assert("console.log(ˇvar);", "console.log(varˇ);");
17021 assert("console.log(varˇ);", "console.log(ˇvar);");
17022
17023 // When outside a bracket and inside, favor jumping to the inside bracket
17024 assert(
17025 "console.log('foo', [1, 2, 3]ˇ);",
17026 "console.log(ˇ'foo', [1, 2, 3]);",
17027 );
17028 assert(
17029 "console.log(ˇ'foo', [1, 2, 3]);",
17030 "console.log('foo', [1, 2, 3]ˇ);",
17031 );
17032
17033 // Bias forward if two options are equally likely
17034 assert(
17035 "let result = curried_fun()ˇ();",
17036 "let result = curried_fun()()ˇ;",
17037 );
17038
17039 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17040 assert(
17041 indoc! {"
17042 function test() {
17043 console.log('test')ˇ
17044 }"},
17045 indoc! {"
17046 function test() {
17047 console.logˇ('test')
17048 }"},
17049 );
17050}
17051
17052#[gpui::test]
17053async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17054 init_test(cx, |_| {});
17055
17056 let fs = FakeFs::new(cx.executor());
17057 fs.insert_tree(
17058 path!("/a"),
17059 json!({
17060 "main.rs": "fn main() { let a = 5; }",
17061 "other.rs": "// Test file",
17062 }),
17063 )
17064 .await;
17065 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17066
17067 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17068 language_registry.add(Arc::new(Language::new(
17069 LanguageConfig {
17070 name: "Rust".into(),
17071 matcher: LanguageMatcher {
17072 path_suffixes: vec!["rs".to_string()],
17073 ..Default::default()
17074 },
17075 brackets: BracketPairConfig {
17076 pairs: vec![BracketPair {
17077 start: "{".to_string(),
17078 end: "}".to_string(),
17079 close: true,
17080 surround: true,
17081 newline: true,
17082 }],
17083 disabled_scopes_by_bracket_ix: Vec::new(),
17084 },
17085 ..Default::default()
17086 },
17087 Some(tree_sitter_rust::LANGUAGE.into()),
17088 )));
17089 let mut fake_servers = language_registry.register_fake_lsp(
17090 "Rust",
17091 FakeLspAdapter {
17092 capabilities: lsp::ServerCapabilities {
17093 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17094 first_trigger_character: "{".to_string(),
17095 more_trigger_character: None,
17096 }),
17097 ..Default::default()
17098 },
17099 ..Default::default()
17100 },
17101 );
17102
17103 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17104
17105 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17106
17107 let worktree_id = workspace
17108 .update(cx, |workspace, _, cx| {
17109 workspace.project().update(cx, |project, cx| {
17110 project.worktrees(cx).next().unwrap().read(cx).id()
17111 })
17112 })
17113 .unwrap();
17114
17115 let buffer = project
17116 .update(cx, |project, cx| {
17117 project.open_local_buffer(path!("/a/main.rs"), cx)
17118 })
17119 .await
17120 .unwrap();
17121 let editor_handle = workspace
17122 .update(cx, |workspace, window, cx| {
17123 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17124 })
17125 .unwrap()
17126 .await
17127 .unwrap()
17128 .downcast::<Editor>()
17129 .unwrap();
17130
17131 cx.executor().start_waiting();
17132 let fake_server = fake_servers.next().await.unwrap();
17133
17134 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17135 |params, _| async move {
17136 assert_eq!(
17137 params.text_document_position.text_document.uri,
17138 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17139 );
17140 assert_eq!(
17141 params.text_document_position.position,
17142 lsp::Position::new(0, 21),
17143 );
17144
17145 Ok(Some(vec![lsp::TextEdit {
17146 new_text: "]".to_string(),
17147 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17148 }]))
17149 },
17150 );
17151
17152 editor_handle.update_in(cx, |editor, window, cx| {
17153 window.focus(&editor.focus_handle(cx));
17154 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17155 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17156 });
17157 editor.handle_input("{", window, cx);
17158 });
17159
17160 cx.executor().run_until_parked();
17161
17162 buffer.update(cx, |buffer, _| {
17163 assert_eq!(
17164 buffer.text(),
17165 "fn main() { let a = {5}; }",
17166 "No extra braces from on type formatting should appear in the buffer"
17167 )
17168 });
17169}
17170
17171#[gpui::test(iterations = 20, seeds(31))]
17172async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17173 init_test(cx, |_| {});
17174
17175 let mut cx = EditorLspTestContext::new_rust(
17176 lsp::ServerCapabilities {
17177 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17178 first_trigger_character: ".".to_string(),
17179 more_trigger_character: None,
17180 }),
17181 ..Default::default()
17182 },
17183 cx,
17184 )
17185 .await;
17186
17187 cx.update_buffer(|buffer, _| {
17188 // This causes autoindent to be async.
17189 buffer.set_sync_parse_timeout(Duration::ZERO)
17190 });
17191
17192 cx.set_state("fn c() {\n d()ˇ\n}\n");
17193 cx.simulate_keystroke("\n");
17194 cx.run_until_parked();
17195
17196 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17197 let mut request =
17198 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17199 let buffer_cloned = buffer_cloned.clone();
17200 async move {
17201 buffer_cloned.update(&mut cx, |buffer, _| {
17202 assert_eq!(
17203 buffer.text(),
17204 "fn c() {\n d()\n .\n}\n",
17205 "OnTypeFormatting should triggered after autoindent applied"
17206 )
17207 })?;
17208
17209 Ok(Some(vec![]))
17210 }
17211 });
17212
17213 cx.simulate_keystroke(".");
17214 cx.run_until_parked();
17215
17216 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17217 assert!(request.next().await.is_some());
17218 request.close();
17219 assert!(request.next().await.is_none());
17220}
17221
17222#[gpui::test]
17223async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17224 init_test(cx, |_| {});
17225
17226 let fs = FakeFs::new(cx.executor());
17227 fs.insert_tree(
17228 path!("/a"),
17229 json!({
17230 "main.rs": "fn main() { let a = 5; }",
17231 "other.rs": "// Test file",
17232 }),
17233 )
17234 .await;
17235
17236 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17237
17238 let server_restarts = Arc::new(AtomicUsize::new(0));
17239 let closure_restarts = Arc::clone(&server_restarts);
17240 let language_server_name = "test language server";
17241 let language_name: LanguageName = "Rust".into();
17242
17243 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17244 language_registry.add(Arc::new(Language::new(
17245 LanguageConfig {
17246 name: language_name.clone(),
17247 matcher: LanguageMatcher {
17248 path_suffixes: vec!["rs".to_string()],
17249 ..Default::default()
17250 },
17251 ..Default::default()
17252 },
17253 Some(tree_sitter_rust::LANGUAGE.into()),
17254 )));
17255 let mut fake_servers = language_registry.register_fake_lsp(
17256 "Rust",
17257 FakeLspAdapter {
17258 name: language_server_name,
17259 initialization_options: Some(json!({
17260 "testOptionValue": true
17261 })),
17262 initializer: Some(Box::new(move |fake_server| {
17263 let task_restarts = Arc::clone(&closure_restarts);
17264 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17265 task_restarts.fetch_add(1, atomic::Ordering::Release);
17266 futures::future::ready(Ok(()))
17267 });
17268 })),
17269 ..Default::default()
17270 },
17271 );
17272
17273 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17274 let _buffer = project
17275 .update(cx, |project, cx| {
17276 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17277 })
17278 .await
17279 .unwrap();
17280 let _fake_server = fake_servers.next().await.unwrap();
17281 update_test_language_settings(cx, |language_settings| {
17282 language_settings.languages.0.insert(
17283 language_name.clone().0,
17284 LanguageSettingsContent {
17285 tab_size: NonZeroU32::new(8),
17286 ..Default::default()
17287 },
17288 );
17289 });
17290 cx.executor().run_until_parked();
17291 assert_eq!(
17292 server_restarts.load(atomic::Ordering::Acquire),
17293 0,
17294 "Should not restart LSP server on an unrelated change"
17295 );
17296
17297 update_test_project_settings(cx, |project_settings| {
17298 project_settings.lsp.insert(
17299 "Some other server name".into(),
17300 LspSettings {
17301 binary: None,
17302 settings: None,
17303 initialization_options: Some(json!({
17304 "some other init value": false
17305 })),
17306 enable_lsp_tasks: false,
17307 fetch: None,
17308 },
17309 );
17310 });
17311 cx.executor().run_until_parked();
17312 assert_eq!(
17313 server_restarts.load(atomic::Ordering::Acquire),
17314 0,
17315 "Should not restart LSP server on an unrelated LSP settings change"
17316 );
17317
17318 update_test_project_settings(cx, |project_settings| {
17319 project_settings.lsp.insert(
17320 language_server_name.into(),
17321 LspSettings {
17322 binary: None,
17323 settings: None,
17324 initialization_options: Some(json!({
17325 "anotherInitValue": false
17326 })),
17327 enable_lsp_tasks: false,
17328 fetch: None,
17329 },
17330 );
17331 });
17332 cx.executor().run_until_parked();
17333 assert_eq!(
17334 server_restarts.load(atomic::Ordering::Acquire),
17335 1,
17336 "Should restart LSP server on a related LSP settings change"
17337 );
17338
17339 update_test_project_settings(cx, |project_settings| {
17340 project_settings.lsp.insert(
17341 language_server_name.into(),
17342 LspSettings {
17343 binary: None,
17344 settings: None,
17345 initialization_options: Some(json!({
17346 "anotherInitValue": false
17347 })),
17348 enable_lsp_tasks: false,
17349 fetch: None,
17350 },
17351 );
17352 });
17353 cx.executor().run_until_parked();
17354 assert_eq!(
17355 server_restarts.load(atomic::Ordering::Acquire),
17356 1,
17357 "Should not restart LSP server on a related LSP settings change that is the same"
17358 );
17359
17360 update_test_project_settings(cx, |project_settings| {
17361 project_settings.lsp.insert(
17362 language_server_name.into(),
17363 LspSettings {
17364 binary: None,
17365 settings: None,
17366 initialization_options: None,
17367 enable_lsp_tasks: false,
17368 fetch: None,
17369 },
17370 );
17371 });
17372 cx.executor().run_until_parked();
17373 assert_eq!(
17374 server_restarts.load(atomic::Ordering::Acquire),
17375 2,
17376 "Should restart LSP server on another related LSP settings change"
17377 );
17378}
17379
17380#[gpui::test]
17381async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17382 init_test(cx, |_| {});
17383
17384 let mut cx = EditorLspTestContext::new_rust(
17385 lsp::ServerCapabilities {
17386 completion_provider: Some(lsp::CompletionOptions {
17387 trigger_characters: Some(vec![".".to_string()]),
17388 resolve_provider: Some(true),
17389 ..Default::default()
17390 }),
17391 ..Default::default()
17392 },
17393 cx,
17394 )
17395 .await;
17396
17397 cx.set_state("fn main() { let a = 2ˇ; }");
17398 cx.simulate_keystroke(".");
17399 let completion_item = lsp::CompletionItem {
17400 label: "some".into(),
17401 kind: Some(lsp::CompletionItemKind::SNIPPET),
17402 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17403 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17404 kind: lsp::MarkupKind::Markdown,
17405 value: "```rust\nSome(2)\n```".to_string(),
17406 })),
17407 deprecated: Some(false),
17408 sort_text: Some("fffffff2".to_string()),
17409 filter_text: Some("some".to_string()),
17410 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17411 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17412 range: lsp::Range {
17413 start: lsp::Position {
17414 line: 0,
17415 character: 22,
17416 },
17417 end: lsp::Position {
17418 line: 0,
17419 character: 22,
17420 },
17421 },
17422 new_text: "Some(2)".to_string(),
17423 })),
17424 additional_text_edits: Some(vec![lsp::TextEdit {
17425 range: lsp::Range {
17426 start: lsp::Position {
17427 line: 0,
17428 character: 20,
17429 },
17430 end: lsp::Position {
17431 line: 0,
17432 character: 22,
17433 },
17434 },
17435 new_text: "".to_string(),
17436 }]),
17437 ..Default::default()
17438 };
17439
17440 let closure_completion_item = completion_item.clone();
17441 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17442 let task_completion_item = closure_completion_item.clone();
17443 async move {
17444 Ok(Some(lsp::CompletionResponse::Array(vec![
17445 task_completion_item,
17446 ])))
17447 }
17448 });
17449
17450 request.next().await;
17451
17452 cx.condition(|editor, _| editor.context_menu_visible())
17453 .await;
17454 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17455 editor
17456 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17457 .unwrap()
17458 });
17459 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17460
17461 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17462 let task_completion_item = completion_item.clone();
17463 async move { Ok(task_completion_item) }
17464 })
17465 .next()
17466 .await
17467 .unwrap();
17468 apply_additional_edits.await.unwrap();
17469 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17470}
17471
17472#[gpui::test]
17473async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17474 init_test(cx, |_| {});
17475
17476 let mut cx = EditorLspTestContext::new_rust(
17477 lsp::ServerCapabilities {
17478 completion_provider: Some(lsp::CompletionOptions {
17479 trigger_characters: Some(vec![".".to_string()]),
17480 resolve_provider: Some(true),
17481 ..Default::default()
17482 }),
17483 ..Default::default()
17484 },
17485 cx,
17486 )
17487 .await;
17488
17489 cx.set_state("fn main() { let a = 2ˇ; }");
17490 cx.simulate_keystroke(".");
17491
17492 let item1 = lsp::CompletionItem {
17493 label: "method id()".to_string(),
17494 filter_text: Some("id".to_string()),
17495 detail: None,
17496 documentation: None,
17497 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17498 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17499 new_text: ".id".to_string(),
17500 })),
17501 ..lsp::CompletionItem::default()
17502 };
17503
17504 let item2 = lsp::CompletionItem {
17505 label: "other".to_string(),
17506 filter_text: Some("other".to_string()),
17507 detail: None,
17508 documentation: None,
17509 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17510 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17511 new_text: ".other".to_string(),
17512 })),
17513 ..lsp::CompletionItem::default()
17514 };
17515
17516 let item1 = item1.clone();
17517 cx.set_request_handler::<lsp::request::Completion, _, _>({
17518 let item1 = item1.clone();
17519 move |_, _, _| {
17520 let item1 = item1.clone();
17521 let item2 = item2.clone();
17522 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17523 }
17524 })
17525 .next()
17526 .await;
17527
17528 cx.condition(|editor, _| editor.context_menu_visible())
17529 .await;
17530 cx.update_editor(|editor, _, _| {
17531 let context_menu = editor.context_menu.borrow_mut();
17532 let context_menu = context_menu
17533 .as_ref()
17534 .expect("Should have the context menu deployed");
17535 match context_menu {
17536 CodeContextMenu::Completions(completions_menu) => {
17537 let completions = completions_menu.completions.borrow_mut();
17538 assert_eq!(
17539 completions
17540 .iter()
17541 .map(|completion| &completion.label.text)
17542 .collect::<Vec<_>>(),
17543 vec!["method id()", "other"]
17544 )
17545 }
17546 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17547 }
17548 });
17549
17550 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17551 let item1 = item1.clone();
17552 move |_, item_to_resolve, _| {
17553 let item1 = item1.clone();
17554 async move {
17555 if item1 == item_to_resolve {
17556 Ok(lsp::CompletionItem {
17557 label: "method id()".to_string(),
17558 filter_text: Some("id".to_string()),
17559 detail: Some("Now resolved!".to_string()),
17560 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17561 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17562 range: lsp::Range::new(
17563 lsp::Position::new(0, 22),
17564 lsp::Position::new(0, 22),
17565 ),
17566 new_text: ".id".to_string(),
17567 })),
17568 ..lsp::CompletionItem::default()
17569 })
17570 } else {
17571 Ok(item_to_resolve)
17572 }
17573 }
17574 }
17575 })
17576 .next()
17577 .await
17578 .unwrap();
17579 cx.run_until_parked();
17580
17581 cx.update_editor(|editor, window, cx| {
17582 editor.context_menu_next(&Default::default(), window, cx);
17583 });
17584
17585 cx.update_editor(|editor, _, _| {
17586 let context_menu = editor.context_menu.borrow_mut();
17587 let context_menu = context_menu
17588 .as_ref()
17589 .expect("Should have the context menu deployed");
17590 match context_menu {
17591 CodeContextMenu::Completions(completions_menu) => {
17592 let completions = completions_menu.completions.borrow_mut();
17593 assert_eq!(
17594 completions
17595 .iter()
17596 .map(|completion| &completion.label.text)
17597 .collect::<Vec<_>>(),
17598 vec!["method id() Now resolved!", "other"],
17599 "Should update first completion label, but not second as the filter text did not match."
17600 );
17601 }
17602 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17603 }
17604 });
17605}
17606
17607#[gpui::test]
17608async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17609 init_test(cx, |_| {});
17610 let mut cx = EditorLspTestContext::new_rust(
17611 lsp::ServerCapabilities {
17612 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17613 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17614 completion_provider: Some(lsp::CompletionOptions {
17615 resolve_provider: Some(true),
17616 ..Default::default()
17617 }),
17618 ..Default::default()
17619 },
17620 cx,
17621 )
17622 .await;
17623 cx.set_state(indoc! {"
17624 struct TestStruct {
17625 field: i32
17626 }
17627
17628 fn mainˇ() {
17629 let unused_var = 42;
17630 let test_struct = TestStruct { field: 42 };
17631 }
17632 "});
17633 let symbol_range = cx.lsp_range(indoc! {"
17634 struct TestStruct {
17635 field: i32
17636 }
17637
17638 «fn main»() {
17639 let unused_var = 42;
17640 let test_struct = TestStruct { field: 42 };
17641 }
17642 "});
17643 let mut hover_requests =
17644 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17645 Ok(Some(lsp::Hover {
17646 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17647 kind: lsp::MarkupKind::Markdown,
17648 value: "Function documentation".to_string(),
17649 }),
17650 range: Some(symbol_range),
17651 }))
17652 });
17653
17654 // Case 1: Test that code action menu hide hover popover
17655 cx.dispatch_action(Hover);
17656 hover_requests.next().await;
17657 cx.condition(|editor, _| editor.hover_state.visible()).await;
17658 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17659 move |_, _, _| async move {
17660 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17661 lsp::CodeAction {
17662 title: "Remove unused variable".to_string(),
17663 kind: Some(CodeActionKind::QUICKFIX),
17664 edit: Some(lsp::WorkspaceEdit {
17665 changes: Some(
17666 [(
17667 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17668 vec![lsp::TextEdit {
17669 range: lsp::Range::new(
17670 lsp::Position::new(5, 4),
17671 lsp::Position::new(5, 27),
17672 ),
17673 new_text: "".to_string(),
17674 }],
17675 )]
17676 .into_iter()
17677 .collect(),
17678 ),
17679 ..Default::default()
17680 }),
17681 ..Default::default()
17682 },
17683 )]))
17684 },
17685 );
17686 cx.update_editor(|editor, window, cx| {
17687 editor.toggle_code_actions(
17688 &ToggleCodeActions {
17689 deployed_from: None,
17690 quick_launch: false,
17691 },
17692 window,
17693 cx,
17694 );
17695 });
17696 code_action_requests.next().await;
17697 cx.run_until_parked();
17698 cx.condition(|editor, _| editor.context_menu_visible())
17699 .await;
17700 cx.update_editor(|editor, _, _| {
17701 assert!(
17702 !editor.hover_state.visible(),
17703 "Hover popover should be hidden when code action menu is shown"
17704 );
17705 // Hide code actions
17706 editor.context_menu.take();
17707 });
17708
17709 // Case 2: Test that code completions hide hover popover
17710 cx.dispatch_action(Hover);
17711 hover_requests.next().await;
17712 cx.condition(|editor, _| editor.hover_state.visible()).await;
17713 let counter = Arc::new(AtomicUsize::new(0));
17714 let mut completion_requests =
17715 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17716 let counter = counter.clone();
17717 async move {
17718 counter.fetch_add(1, atomic::Ordering::Release);
17719 Ok(Some(lsp::CompletionResponse::Array(vec![
17720 lsp::CompletionItem {
17721 label: "main".into(),
17722 kind: Some(lsp::CompletionItemKind::FUNCTION),
17723 detail: Some("() -> ()".to_string()),
17724 ..Default::default()
17725 },
17726 lsp::CompletionItem {
17727 label: "TestStruct".into(),
17728 kind: Some(lsp::CompletionItemKind::STRUCT),
17729 detail: Some("struct TestStruct".to_string()),
17730 ..Default::default()
17731 },
17732 ])))
17733 }
17734 });
17735 cx.update_editor(|editor, window, cx| {
17736 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17737 });
17738 completion_requests.next().await;
17739 cx.condition(|editor, _| editor.context_menu_visible())
17740 .await;
17741 cx.update_editor(|editor, _, _| {
17742 assert!(
17743 !editor.hover_state.visible(),
17744 "Hover popover should be hidden when completion menu is shown"
17745 );
17746 });
17747}
17748
17749#[gpui::test]
17750async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17751 init_test(cx, |_| {});
17752
17753 let mut cx = EditorLspTestContext::new_rust(
17754 lsp::ServerCapabilities {
17755 completion_provider: Some(lsp::CompletionOptions {
17756 trigger_characters: Some(vec![".".to_string()]),
17757 resolve_provider: Some(true),
17758 ..Default::default()
17759 }),
17760 ..Default::default()
17761 },
17762 cx,
17763 )
17764 .await;
17765
17766 cx.set_state("fn main() { let a = 2ˇ; }");
17767 cx.simulate_keystroke(".");
17768
17769 let unresolved_item_1 = lsp::CompletionItem {
17770 label: "id".to_string(),
17771 filter_text: Some("id".to_string()),
17772 detail: None,
17773 documentation: None,
17774 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17775 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17776 new_text: ".id".to_string(),
17777 })),
17778 ..lsp::CompletionItem::default()
17779 };
17780 let resolved_item_1 = lsp::CompletionItem {
17781 additional_text_edits: Some(vec![lsp::TextEdit {
17782 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17783 new_text: "!!".to_string(),
17784 }]),
17785 ..unresolved_item_1.clone()
17786 };
17787 let unresolved_item_2 = lsp::CompletionItem {
17788 label: "other".to_string(),
17789 filter_text: Some("other".to_string()),
17790 detail: None,
17791 documentation: None,
17792 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17793 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17794 new_text: ".other".to_string(),
17795 })),
17796 ..lsp::CompletionItem::default()
17797 };
17798 let resolved_item_2 = lsp::CompletionItem {
17799 additional_text_edits: Some(vec![lsp::TextEdit {
17800 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17801 new_text: "??".to_string(),
17802 }]),
17803 ..unresolved_item_2.clone()
17804 };
17805
17806 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17807 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17808 cx.lsp
17809 .server
17810 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17811 let unresolved_item_1 = unresolved_item_1.clone();
17812 let resolved_item_1 = resolved_item_1.clone();
17813 let unresolved_item_2 = unresolved_item_2.clone();
17814 let resolved_item_2 = resolved_item_2.clone();
17815 let resolve_requests_1 = resolve_requests_1.clone();
17816 let resolve_requests_2 = resolve_requests_2.clone();
17817 move |unresolved_request, _| {
17818 let unresolved_item_1 = unresolved_item_1.clone();
17819 let resolved_item_1 = resolved_item_1.clone();
17820 let unresolved_item_2 = unresolved_item_2.clone();
17821 let resolved_item_2 = resolved_item_2.clone();
17822 let resolve_requests_1 = resolve_requests_1.clone();
17823 let resolve_requests_2 = resolve_requests_2.clone();
17824 async move {
17825 if unresolved_request == unresolved_item_1 {
17826 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17827 Ok(resolved_item_1.clone())
17828 } else if unresolved_request == unresolved_item_2 {
17829 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17830 Ok(resolved_item_2.clone())
17831 } else {
17832 panic!("Unexpected completion item {unresolved_request:?}")
17833 }
17834 }
17835 }
17836 })
17837 .detach();
17838
17839 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17840 let unresolved_item_1 = unresolved_item_1.clone();
17841 let unresolved_item_2 = unresolved_item_2.clone();
17842 async move {
17843 Ok(Some(lsp::CompletionResponse::Array(vec![
17844 unresolved_item_1,
17845 unresolved_item_2,
17846 ])))
17847 }
17848 })
17849 .next()
17850 .await;
17851
17852 cx.condition(|editor, _| editor.context_menu_visible())
17853 .await;
17854 cx.update_editor(|editor, _, _| {
17855 let context_menu = editor.context_menu.borrow_mut();
17856 let context_menu = context_menu
17857 .as_ref()
17858 .expect("Should have the context menu deployed");
17859 match context_menu {
17860 CodeContextMenu::Completions(completions_menu) => {
17861 let completions = completions_menu.completions.borrow_mut();
17862 assert_eq!(
17863 completions
17864 .iter()
17865 .map(|completion| &completion.label.text)
17866 .collect::<Vec<_>>(),
17867 vec!["id", "other"]
17868 )
17869 }
17870 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17871 }
17872 });
17873 cx.run_until_parked();
17874
17875 cx.update_editor(|editor, window, cx| {
17876 editor.context_menu_next(&ContextMenuNext, window, cx);
17877 });
17878 cx.run_until_parked();
17879 cx.update_editor(|editor, window, cx| {
17880 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17881 });
17882 cx.run_until_parked();
17883 cx.update_editor(|editor, window, cx| {
17884 editor.context_menu_next(&ContextMenuNext, window, cx);
17885 });
17886 cx.run_until_parked();
17887 cx.update_editor(|editor, window, cx| {
17888 editor
17889 .compose_completion(&ComposeCompletion::default(), window, cx)
17890 .expect("No task returned")
17891 })
17892 .await
17893 .expect("Completion failed");
17894 cx.run_until_parked();
17895
17896 cx.update_editor(|editor, _, cx| {
17897 assert_eq!(
17898 resolve_requests_1.load(atomic::Ordering::Acquire),
17899 1,
17900 "Should always resolve once despite multiple selections"
17901 );
17902 assert_eq!(
17903 resolve_requests_2.load(atomic::Ordering::Acquire),
17904 1,
17905 "Should always resolve once after multiple selections and applying the completion"
17906 );
17907 assert_eq!(
17908 editor.text(cx),
17909 "fn main() { let a = ??.other; }",
17910 "Should use resolved data when applying the completion"
17911 );
17912 });
17913}
17914
17915#[gpui::test]
17916async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17917 init_test(cx, |_| {});
17918
17919 let item_0 = lsp::CompletionItem {
17920 label: "abs".into(),
17921 insert_text: Some("abs".into()),
17922 data: Some(json!({ "very": "special"})),
17923 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17924 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17925 lsp::InsertReplaceEdit {
17926 new_text: "abs".to_string(),
17927 insert: lsp::Range::default(),
17928 replace: lsp::Range::default(),
17929 },
17930 )),
17931 ..lsp::CompletionItem::default()
17932 };
17933 let items = iter::once(item_0.clone())
17934 .chain((11..51).map(|i| lsp::CompletionItem {
17935 label: format!("item_{}", i),
17936 insert_text: Some(format!("item_{}", i)),
17937 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17938 ..lsp::CompletionItem::default()
17939 }))
17940 .collect::<Vec<_>>();
17941
17942 let default_commit_characters = vec!["?".to_string()];
17943 let default_data = json!({ "default": "data"});
17944 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17945 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17946 let default_edit_range = lsp::Range {
17947 start: lsp::Position {
17948 line: 0,
17949 character: 5,
17950 },
17951 end: lsp::Position {
17952 line: 0,
17953 character: 5,
17954 },
17955 };
17956
17957 let mut cx = EditorLspTestContext::new_rust(
17958 lsp::ServerCapabilities {
17959 completion_provider: Some(lsp::CompletionOptions {
17960 trigger_characters: Some(vec![".".to_string()]),
17961 resolve_provider: Some(true),
17962 ..Default::default()
17963 }),
17964 ..Default::default()
17965 },
17966 cx,
17967 )
17968 .await;
17969
17970 cx.set_state("fn main() { let a = 2ˇ; }");
17971 cx.simulate_keystroke(".");
17972
17973 let completion_data = default_data.clone();
17974 let completion_characters = default_commit_characters.clone();
17975 let completion_items = items.clone();
17976 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17977 let default_data = completion_data.clone();
17978 let default_commit_characters = completion_characters.clone();
17979 let items = completion_items.clone();
17980 async move {
17981 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17982 items,
17983 item_defaults: Some(lsp::CompletionListItemDefaults {
17984 data: Some(default_data.clone()),
17985 commit_characters: Some(default_commit_characters.clone()),
17986 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17987 default_edit_range,
17988 )),
17989 insert_text_format: Some(default_insert_text_format),
17990 insert_text_mode: Some(default_insert_text_mode),
17991 }),
17992 ..lsp::CompletionList::default()
17993 })))
17994 }
17995 })
17996 .next()
17997 .await;
17998
17999 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18000 cx.lsp
18001 .server
18002 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18003 let closure_resolved_items = resolved_items.clone();
18004 move |item_to_resolve, _| {
18005 let closure_resolved_items = closure_resolved_items.clone();
18006 async move {
18007 closure_resolved_items.lock().push(item_to_resolve.clone());
18008 Ok(item_to_resolve)
18009 }
18010 }
18011 })
18012 .detach();
18013
18014 cx.condition(|editor, _| editor.context_menu_visible())
18015 .await;
18016 cx.run_until_parked();
18017 cx.update_editor(|editor, _, _| {
18018 let menu = editor.context_menu.borrow_mut();
18019 match menu.as_ref().expect("should have the completions menu") {
18020 CodeContextMenu::Completions(completions_menu) => {
18021 assert_eq!(
18022 completions_menu
18023 .entries
18024 .borrow()
18025 .iter()
18026 .map(|mat| mat.string.clone())
18027 .collect::<Vec<String>>(),
18028 items
18029 .iter()
18030 .map(|completion| completion.label.clone())
18031 .collect::<Vec<String>>()
18032 );
18033 }
18034 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18035 }
18036 });
18037 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18038 // with 4 from the end.
18039 assert_eq!(
18040 *resolved_items.lock(),
18041 [&items[0..16], &items[items.len() - 4..items.len()]]
18042 .concat()
18043 .iter()
18044 .cloned()
18045 .map(|mut item| {
18046 if item.data.is_none() {
18047 item.data = Some(default_data.clone());
18048 }
18049 item
18050 })
18051 .collect::<Vec<lsp::CompletionItem>>(),
18052 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18053 );
18054 resolved_items.lock().clear();
18055
18056 cx.update_editor(|editor, window, cx| {
18057 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18058 });
18059 cx.run_until_parked();
18060 // Completions that have already been resolved are skipped.
18061 assert_eq!(
18062 *resolved_items.lock(),
18063 items[items.len() - 17..items.len() - 4]
18064 .iter()
18065 .cloned()
18066 .map(|mut item| {
18067 if item.data.is_none() {
18068 item.data = Some(default_data.clone());
18069 }
18070 item
18071 })
18072 .collect::<Vec<lsp::CompletionItem>>()
18073 );
18074 resolved_items.lock().clear();
18075}
18076
18077#[gpui::test]
18078async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18079 init_test(cx, |_| {});
18080
18081 let mut cx = EditorLspTestContext::new(
18082 Language::new(
18083 LanguageConfig {
18084 matcher: LanguageMatcher {
18085 path_suffixes: vec!["jsx".into()],
18086 ..Default::default()
18087 },
18088 overrides: [(
18089 "element".into(),
18090 LanguageConfigOverride {
18091 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18092 ..Default::default()
18093 },
18094 )]
18095 .into_iter()
18096 .collect(),
18097 ..Default::default()
18098 },
18099 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18100 )
18101 .with_override_query("(jsx_self_closing_element) @element")
18102 .unwrap(),
18103 lsp::ServerCapabilities {
18104 completion_provider: Some(lsp::CompletionOptions {
18105 trigger_characters: Some(vec![":".to_string()]),
18106 ..Default::default()
18107 }),
18108 ..Default::default()
18109 },
18110 cx,
18111 )
18112 .await;
18113
18114 cx.lsp
18115 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18116 Ok(Some(lsp::CompletionResponse::Array(vec![
18117 lsp::CompletionItem {
18118 label: "bg-blue".into(),
18119 ..Default::default()
18120 },
18121 lsp::CompletionItem {
18122 label: "bg-red".into(),
18123 ..Default::default()
18124 },
18125 lsp::CompletionItem {
18126 label: "bg-yellow".into(),
18127 ..Default::default()
18128 },
18129 ])))
18130 });
18131
18132 cx.set_state(r#"<p class="bgˇ" />"#);
18133
18134 // Trigger completion when typing a dash, because the dash is an extra
18135 // word character in the 'element' scope, which contains the cursor.
18136 cx.simulate_keystroke("-");
18137 cx.executor().run_until_parked();
18138 cx.update_editor(|editor, _, _| {
18139 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18140 {
18141 assert_eq!(
18142 completion_menu_entries(menu),
18143 &["bg-blue", "bg-red", "bg-yellow"]
18144 );
18145 } else {
18146 panic!("expected completion menu to be open");
18147 }
18148 });
18149
18150 cx.simulate_keystroke("l");
18151 cx.executor().run_until_parked();
18152 cx.update_editor(|editor, _, _| {
18153 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18154 {
18155 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18156 } else {
18157 panic!("expected completion menu to be open");
18158 }
18159 });
18160
18161 // When filtering completions, consider the character after the '-' to
18162 // be the start of a subword.
18163 cx.set_state(r#"<p class="yelˇ" />"#);
18164 cx.simulate_keystroke("l");
18165 cx.executor().run_until_parked();
18166 cx.update_editor(|editor, _, _| {
18167 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18168 {
18169 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18170 } else {
18171 panic!("expected completion menu to be open");
18172 }
18173 });
18174}
18175
18176fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18177 let entries = menu.entries.borrow();
18178 entries.iter().map(|mat| mat.string.clone()).collect()
18179}
18180
18181#[gpui::test]
18182async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18183 init_test(cx, |settings| {
18184 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
18185 });
18186
18187 let fs = FakeFs::new(cx.executor());
18188 fs.insert_file(path!("/file.ts"), Default::default()).await;
18189
18190 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18191 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18192
18193 language_registry.add(Arc::new(Language::new(
18194 LanguageConfig {
18195 name: "TypeScript".into(),
18196 matcher: LanguageMatcher {
18197 path_suffixes: vec!["ts".to_string()],
18198 ..Default::default()
18199 },
18200 ..Default::default()
18201 },
18202 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18203 )));
18204 update_test_language_settings(cx, |settings| {
18205 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18206 });
18207
18208 let test_plugin = "test_plugin";
18209 let _ = language_registry.register_fake_lsp(
18210 "TypeScript",
18211 FakeLspAdapter {
18212 prettier_plugins: vec![test_plugin],
18213 ..Default::default()
18214 },
18215 );
18216
18217 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18218 let buffer = project
18219 .update(cx, |project, cx| {
18220 project.open_local_buffer(path!("/file.ts"), cx)
18221 })
18222 .await
18223 .unwrap();
18224
18225 let buffer_text = "one\ntwo\nthree\n";
18226 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18227 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18228 editor.update_in(cx, |editor, window, cx| {
18229 editor.set_text(buffer_text, window, cx)
18230 });
18231
18232 editor
18233 .update_in(cx, |editor, window, cx| {
18234 editor.perform_format(
18235 project.clone(),
18236 FormatTrigger::Manual,
18237 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18238 window,
18239 cx,
18240 )
18241 })
18242 .unwrap()
18243 .await;
18244 assert_eq!(
18245 editor.update(cx, |editor, cx| editor.text(cx)),
18246 buffer_text.to_string() + prettier_format_suffix,
18247 "Test prettier formatting was not applied to the original buffer text",
18248 );
18249
18250 update_test_language_settings(cx, |settings| {
18251 settings.defaults.formatter = Some(FormatterList::default())
18252 });
18253 let format = editor.update_in(cx, |editor, window, cx| {
18254 editor.perform_format(
18255 project.clone(),
18256 FormatTrigger::Manual,
18257 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18258 window,
18259 cx,
18260 )
18261 });
18262 format.await.unwrap();
18263 assert_eq!(
18264 editor.update(cx, |editor, cx| editor.text(cx)),
18265 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18266 "Autoformatting (via test prettier) was not applied to the original buffer text",
18267 );
18268}
18269
18270#[gpui::test]
18271async fn test_addition_reverts(cx: &mut TestAppContext) {
18272 init_test(cx, |_| {});
18273 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18274 let base_text = indoc! {r#"
18275 struct Row;
18276 struct Row1;
18277 struct Row2;
18278
18279 struct Row4;
18280 struct Row5;
18281 struct Row6;
18282
18283 struct Row8;
18284 struct Row9;
18285 struct Row10;"#};
18286
18287 // When addition hunks are not adjacent to carets, no hunk revert is performed
18288 assert_hunk_revert(
18289 indoc! {r#"struct Row;
18290 struct Row1;
18291 struct Row1.1;
18292 struct Row1.2;
18293 struct Row2;ˇ
18294
18295 struct Row4;
18296 struct Row5;
18297 struct Row6;
18298
18299 struct Row8;
18300 ˇstruct Row9;
18301 struct Row9.1;
18302 struct Row9.2;
18303 struct Row9.3;
18304 struct Row10;"#},
18305 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18306 indoc! {r#"struct Row;
18307 struct Row1;
18308 struct Row1.1;
18309 struct Row1.2;
18310 struct Row2;ˇ
18311
18312 struct Row4;
18313 struct Row5;
18314 struct Row6;
18315
18316 struct Row8;
18317 ˇstruct Row9;
18318 struct Row9.1;
18319 struct Row9.2;
18320 struct Row9.3;
18321 struct Row10;"#},
18322 base_text,
18323 &mut cx,
18324 );
18325 // Same for selections
18326 assert_hunk_revert(
18327 indoc! {r#"struct Row;
18328 struct Row1;
18329 struct Row2;
18330 struct Row2.1;
18331 struct Row2.2;
18332 «ˇ
18333 struct Row4;
18334 struct» Row5;
18335 «struct Row6;
18336 ˇ»
18337 struct Row9.1;
18338 struct Row9.2;
18339 struct Row9.3;
18340 struct Row8;
18341 struct Row9;
18342 struct Row10;"#},
18343 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18344 indoc! {r#"struct Row;
18345 struct Row1;
18346 struct Row2;
18347 struct Row2.1;
18348 struct Row2.2;
18349 «ˇ
18350 struct Row4;
18351 struct» Row5;
18352 «struct Row6;
18353 ˇ»
18354 struct Row9.1;
18355 struct Row9.2;
18356 struct Row9.3;
18357 struct Row8;
18358 struct Row9;
18359 struct Row10;"#},
18360 base_text,
18361 &mut cx,
18362 );
18363
18364 // When carets and selections intersect the addition hunks, those are reverted.
18365 // Adjacent carets got merged.
18366 assert_hunk_revert(
18367 indoc! {r#"struct Row;
18368 ˇ// something on the top
18369 struct Row1;
18370 struct Row2;
18371 struct Roˇw3.1;
18372 struct Row2.2;
18373 struct Row2.3;ˇ
18374
18375 struct Row4;
18376 struct ˇRow5.1;
18377 struct Row5.2;
18378 struct «Rowˇ»5.3;
18379 struct Row5;
18380 struct Row6;
18381 ˇ
18382 struct Row9.1;
18383 struct «Rowˇ»9.2;
18384 struct «ˇRow»9.3;
18385 struct Row8;
18386 struct Row9;
18387 «ˇ// something on bottom»
18388 struct Row10;"#},
18389 vec![
18390 DiffHunkStatusKind::Added,
18391 DiffHunkStatusKind::Added,
18392 DiffHunkStatusKind::Added,
18393 DiffHunkStatusKind::Added,
18394 DiffHunkStatusKind::Added,
18395 ],
18396 indoc! {r#"struct Row;
18397 ˇstruct Row1;
18398 struct Row2;
18399 ˇ
18400 struct Row4;
18401 ˇstruct Row5;
18402 struct Row6;
18403 ˇ
18404 ˇstruct Row8;
18405 struct Row9;
18406 ˇstruct Row10;"#},
18407 base_text,
18408 &mut cx,
18409 );
18410}
18411
18412#[gpui::test]
18413async fn test_modification_reverts(cx: &mut TestAppContext) {
18414 init_test(cx, |_| {});
18415 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18416 let base_text = indoc! {r#"
18417 struct Row;
18418 struct Row1;
18419 struct Row2;
18420
18421 struct Row4;
18422 struct Row5;
18423 struct Row6;
18424
18425 struct Row8;
18426 struct Row9;
18427 struct Row10;"#};
18428
18429 // Modification hunks behave the same as the addition ones.
18430 assert_hunk_revert(
18431 indoc! {r#"struct Row;
18432 struct Row1;
18433 struct Row33;
18434 ˇ
18435 struct Row4;
18436 struct Row5;
18437 struct Row6;
18438 ˇ
18439 struct Row99;
18440 struct Row9;
18441 struct Row10;"#},
18442 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18443 indoc! {r#"struct Row;
18444 struct Row1;
18445 struct Row33;
18446 ˇ
18447 struct Row4;
18448 struct Row5;
18449 struct Row6;
18450 ˇ
18451 struct Row99;
18452 struct Row9;
18453 struct Row10;"#},
18454 base_text,
18455 &mut cx,
18456 );
18457 assert_hunk_revert(
18458 indoc! {r#"struct Row;
18459 struct Row1;
18460 struct Row33;
18461 «ˇ
18462 struct Row4;
18463 struct» Row5;
18464 «struct Row6;
18465 ˇ»
18466 struct Row99;
18467 struct Row9;
18468 struct Row10;"#},
18469 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18470 indoc! {r#"struct Row;
18471 struct Row1;
18472 struct Row33;
18473 «ˇ
18474 struct Row4;
18475 struct» Row5;
18476 «struct Row6;
18477 ˇ»
18478 struct Row99;
18479 struct Row9;
18480 struct Row10;"#},
18481 base_text,
18482 &mut cx,
18483 );
18484
18485 assert_hunk_revert(
18486 indoc! {r#"ˇstruct Row1.1;
18487 struct Row1;
18488 «ˇstr»uct Row22;
18489
18490 struct ˇRow44;
18491 struct Row5;
18492 struct «Rˇ»ow66;ˇ
18493
18494 «struˇ»ct Row88;
18495 struct Row9;
18496 struct Row1011;ˇ"#},
18497 vec![
18498 DiffHunkStatusKind::Modified,
18499 DiffHunkStatusKind::Modified,
18500 DiffHunkStatusKind::Modified,
18501 DiffHunkStatusKind::Modified,
18502 DiffHunkStatusKind::Modified,
18503 DiffHunkStatusKind::Modified,
18504 ],
18505 indoc! {r#"struct Row;
18506 ˇstruct Row1;
18507 struct Row2;
18508 ˇ
18509 struct Row4;
18510 ˇstruct Row5;
18511 struct Row6;
18512 ˇ
18513 struct Row8;
18514 ˇstruct Row9;
18515 struct Row10;ˇ"#},
18516 base_text,
18517 &mut cx,
18518 );
18519}
18520
18521#[gpui::test]
18522async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18523 init_test(cx, |_| {});
18524 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18525 let base_text = indoc! {r#"
18526 one
18527
18528 two
18529 three
18530 "#};
18531
18532 cx.set_head_text(base_text);
18533 cx.set_state("\nˇ\n");
18534 cx.executor().run_until_parked();
18535 cx.update_editor(|editor, _window, cx| {
18536 editor.expand_selected_diff_hunks(cx);
18537 });
18538 cx.executor().run_until_parked();
18539 cx.update_editor(|editor, window, cx| {
18540 editor.backspace(&Default::default(), window, cx);
18541 });
18542 cx.run_until_parked();
18543 cx.assert_state_with_diff(
18544 indoc! {r#"
18545
18546 - two
18547 - threeˇ
18548 +
18549 "#}
18550 .to_string(),
18551 );
18552}
18553
18554#[gpui::test]
18555async fn test_deletion_reverts(cx: &mut TestAppContext) {
18556 init_test(cx, |_| {});
18557 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18558 let base_text = indoc! {r#"struct Row;
18559struct Row1;
18560struct Row2;
18561
18562struct Row4;
18563struct Row5;
18564struct Row6;
18565
18566struct Row8;
18567struct Row9;
18568struct Row10;"#};
18569
18570 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18571 assert_hunk_revert(
18572 indoc! {r#"struct Row;
18573 struct Row2;
18574
18575 ˇstruct Row4;
18576 struct Row5;
18577 struct Row6;
18578 ˇ
18579 struct Row8;
18580 struct Row10;"#},
18581 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18582 indoc! {r#"struct Row;
18583 struct Row2;
18584
18585 ˇstruct Row4;
18586 struct Row5;
18587 struct Row6;
18588 ˇ
18589 struct Row8;
18590 struct Row10;"#},
18591 base_text,
18592 &mut cx,
18593 );
18594 assert_hunk_revert(
18595 indoc! {r#"struct Row;
18596 struct Row2;
18597
18598 «ˇstruct Row4;
18599 struct» Row5;
18600 «struct Row6;
18601 ˇ»
18602 struct Row8;
18603 struct Row10;"#},
18604 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18605 indoc! {r#"struct Row;
18606 struct Row2;
18607
18608 «ˇstruct Row4;
18609 struct» Row5;
18610 «struct Row6;
18611 ˇ»
18612 struct Row8;
18613 struct Row10;"#},
18614 base_text,
18615 &mut cx,
18616 );
18617
18618 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18619 assert_hunk_revert(
18620 indoc! {r#"struct Row;
18621 ˇstruct Row2;
18622
18623 struct Row4;
18624 struct Row5;
18625 struct Row6;
18626
18627 struct Row8;ˇ
18628 struct Row10;"#},
18629 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18630 indoc! {r#"struct Row;
18631 struct Row1;
18632 ˇstruct Row2;
18633
18634 struct Row4;
18635 struct Row5;
18636 struct Row6;
18637
18638 struct Row8;ˇ
18639 struct Row9;
18640 struct Row10;"#},
18641 base_text,
18642 &mut cx,
18643 );
18644 assert_hunk_revert(
18645 indoc! {r#"struct Row;
18646 struct Row2«ˇ;
18647 struct Row4;
18648 struct» Row5;
18649 «struct Row6;
18650
18651 struct Row8;ˇ»
18652 struct Row10;"#},
18653 vec![
18654 DiffHunkStatusKind::Deleted,
18655 DiffHunkStatusKind::Deleted,
18656 DiffHunkStatusKind::Deleted,
18657 ],
18658 indoc! {r#"struct Row;
18659 struct Row1;
18660 struct Row2«ˇ;
18661
18662 struct Row4;
18663 struct» Row5;
18664 «struct Row6;
18665
18666 struct Row8;ˇ»
18667 struct Row9;
18668 struct Row10;"#},
18669 base_text,
18670 &mut cx,
18671 );
18672}
18673
18674#[gpui::test]
18675async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18676 init_test(cx, |_| {});
18677
18678 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18679 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18680 let base_text_3 =
18681 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18682
18683 let text_1 = edit_first_char_of_every_line(base_text_1);
18684 let text_2 = edit_first_char_of_every_line(base_text_2);
18685 let text_3 = edit_first_char_of_every_line(base_text_3);
18686
18687 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18688 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18689 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18690
18691 let multibuffer = cx.new(|cx| {
18692 let mut multibuffer = MultiBuffer::new(ReadWrite);
18693 multibuffer.push_excerpts(
18694 buffer_1.clone(),
18695 [
18696 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18697 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18698 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18699 ],
18700 cx,
18701 );
18702 multibuffer.push_excerpts(
18703 buffer_2.clone(),
18704 [
18705 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18706 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18707 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18708 ],
18709 cx,
18710 );
18711 multibuffer.push_excerpts(
18712 buffer_3.clone(),
18713 [
18714 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18715 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18716 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18717 ],
18718 cx,
18719 );
18720 multibuffer
18721 });
18722
18723 let fs = FakeFs::new(cx.executor());
18724 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18725 let (editor, cx) = cx
18726 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18727 editor.update_in(cx, |editor, _window, cx| {
18728 for (buffer, diff_base) in [
18729 (buffer_1.clone(), base_text_1),
18730 (buffer_2.clone(), base_text_2),
18731 (buffer_3.clone(), base_text_3),
18732 ] {
18733 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18734 editor
18735 .buffer
18736 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18737 }
18738 });
18739 cx.executor().run_until_parked();
18740
18741 editor.update_in(cx, |editor, window, cx| {
18742 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}");
18743 editor.select_all(&SelectAll, window, cx);
18744 editor.git_restore(&Default::default(), window, cx);
18745 });
18746 cx.executor().run_until_parked();
18747
18748 // When all ranges are selected, all buffer hunks are reverted.
18749 editor.update(cx, |editor, cx| {
18750 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");
18751 });
18752 buffer_1.update(cx, |buffer, _| {
18753 assert_eq!(buffer.text(), base_text_1);
18754 });
18755 buffer_2.update(cx, |buffer, _| {
18756 assert_eq!(buffer.text(), base_text_2);
18757 });
18758 buffer_3.update(cx, |buffer, _| {
18759 assert_eq!(buffer.text(), base_text_3);
18760 });
18761
18762 editor.update_in(cx, |editor, window, cx| {
18763 editor.undo(&Default::default(), window, cx);
18764 });
18765
18766 editor.update_in(cx, |editor, window, cx| {
18767 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18768 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18769 });
18770 editor.git_restore(&Default::default(), window, cx);
18771 });
18772
18773 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18774 // but not affect buffer_2 and its related excerpts.
18775 editor.update(cx, |editor, cx| {
18776 assert_eq!(
18777 editor.text(cx),
18778 "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}"
18779 );
18780 });
18781 buffer_1.update(cx, |buffer, _| {
18782 assert_eq!(buffer.text(), base_text_1);
18783 });
18784 buffer_2.update(cx, |buffer, _| {
18785 assert_eq!(
18786 buffer.text(),
18787 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18788 );
18789 });
18790 buffer_3.update(cx, |buffer, _| {
18791 assert_eq!(
18792 buffer.text(),
18793 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18794 );
18795 });
18796
18797 fn edit_first_char_of_every_line(text: &str) -> String {
18798 text.split('\n')
18799 .map(|line| format!("X{}", &line[1..]))
18800 .collect::<Vec<_>>()
18801 .join("\n")
18802 }
18803}
18804
18805#[gpui::test]
18806async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18807 init_test(cx, |_| {});
18808
18809 let cols = 4;
18810 let rows = 10;
18811 let sample_text_1 = sample_text(rows, cols, 'a');
18812 assert_eq!(
18813 sample_text_1,
18814 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18815 );
18816 let sample_text_2 = sample_text(rows, cols, 'l');
18817 assert_eq!(
18818 sample_text_2,
18819 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18820 );
18821 let sample_text_3 = sample_text(rows, cols, 'v');
18822 assert_eq!(
18823 sample_text_3,
18824 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18825 );
18826
18827 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18828 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18829 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18830
18831 let multi_buffer = cx.new(|cx| {
18832 let mut multibuffer = MultiBuffer::new(ReadWrite);
18833 multibuffer.push_excerpts(
18834 buffer_1.clone(),
18835 [
18836 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18837 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18838 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18839 ],
18840 cx,
18841 );
18842 multibuffer.push_excerpts(
18843 buffer_2.clone(),
18844 [
18845 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18846 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18847 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18848 ],
18849 cx,
18850 );
18851 multibuffer.push_excerpts(
18852 buffer_3.clone(),
18853 [
18854 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18855 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18856 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18857 ],
18858 cx,
18859 );
18860 multibuffer
18861 });
18862
18863 let fs = FakeFs::new(cx.executor());
18864 fs.insert_tree(
18865 "/a",
18866 json!({
18867 "main.rs": sample_text_1,
18868 "other.rs": sample_text_2,
18869 "lib.rs": sample_text_3,
18870 }),
18871 )
18872 .await;
18873 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18874 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18875 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18876 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18877 Editor::new(
18878 EditorMode::full(),
18879 multi_buffer,
18880 Some(project.clone()),
18881 window,
18882 cx,
18883 )
18884 });
18885 let multibuffer_item_id = workspace
18886 .update(cx, |workspace, window, cx| {
18887 assert!(
18888 workspace.active_item(cx).is_none(),
18889 "active item should be None before the first item is added"
18890 );
18891 workspace.add_item_to_active_pane(
18892 Box::new(multi_buffer_editor.clone()),
18893 None,
18894 true,
18895 window,
18896 cx,
18897 );
18898 let active_item = workspace
18899 .active_item(cx)
18900 .expect("should have an active item after adding the multi buffer");
18901 assert_eq!(
18902 active_item.buffer_kind(cx),
18903 ItemBufferKind::Multibuffer,
18904 "A multi buffer was expected to active after adding"
18905 );
18906 active_item.item_id()
18907 })
18908 .unwrap();
18909 cx.executor().run_until_parked();
18910
18911 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18912 editor.change_selections(
18913 SelectionEffects::scroll(Autoscroll::Next),
18914 window,
18915 cx,
18916 |s| s.select_ranges(Some(1..2)),
18917 );
18918 editor.open_excerpts(&OpenExcerpts, window, cx);
18919 });
18920 cx.executor().run_until_parked();
18921 let first_item_id = workspace
18922 .update(cx, |workspace, window, cx| {
18923 let active_item = workspace
18924 .active_item(cx)
18925 .expect("should have an active item after navigating into the 1st buffer");
18926 let first_item_id = active_item.item_id();
18927 assert_ne!(
18928 first_item_id, multibuffer_item_id,
18929 "Should navigate into the 1st buffer and activate it"
18930 );
18931 assert_eq!(
18932 active_item.buffer_kind(cx),
18933 ItemBufferKind::Singleton,
18934 "New active item should be a singleton buffer"
18935 );
18936 assert_eq!(
18937 active_item
18938 .act_as::<Editor>(cx)
18939 .expect("should have navigated into an editor for the 1st buffer")
18940 .read(cx)
18941 .text(cx),
18942 sample_text_1
18943 );
18944
18945 workspace
18946 .go_back(workspace.active_pane().downgrade(), window, cx)
18947 .detach_and_log_err(cx);
18948
18949 first_item_id
18950 })
18951 .unwrap();
18952 cx.executor().run_until_parked();
18953 workspace
18954 .update(cx, |workspace, _, cx| {
18955 let active_item = workspace
18956 .active_item(cx)
18957 .expect("should have an active item after navigating back");
18958 assert_eq!(
18959 active_item.item_id(),
18960 multibuffer_item_id,
18961 "Should navigate back to the multi buffer"
18962 );
18963 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
18964 })
18965 .unwrap();
18966
18967 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18968 editor.change_selections(
18969 SelectionEffects::scroll(Autoscroll::Next),
18970 window,
18971 cx,
18972 |s| s.select_ranges(Some(39..40)),
18973 );
18974 editor.open_excerpts(&OpenExcerpts, window, cx);
18975 });
18976 cx.executor().run_until_parked();
18977 let second_item_id = workspace
18978 .update(cx, |workspace, window, cx| {
18979 let active_item = workspace
18980 .active_item(cx)
18981 .expect("should have an active item after navigating into the 2nd buffer");
18982 let second_item_id = active_item.item_id();
18983 assert_ne!(
18984 second_item_id, multibuffer_item_id,
18985 "Should navigate away from the multibuffer"
18986 );
18987 assert_ne!(
18988 second_item_id, first_item_id,
18989 "Should navigate into the 2nd buffer and activate it"
18990 );
18991 assert_eq!(
18992 active_item.buffer_kind(cx),
18993 ItemBufferKind::Singleton,
18994 "New active item should be a singleton buffer"
18995 );
18996 assert_eq!(
18997 active_item
18998 .act_as::<Editor>(cx)
18999 .expect("should have navigated into an editor")
19000 .read(cx)
19001 .text(cx),
19002 sample_text_2
19003 );
19004
19005 workspace
19006 .go_back(workspace.active_pane().downgrade(), window, cx)
19007 .detach_and_log_err(cx);
19008
19009 second_item_id
19010 })
19011 .unwrap();
19012 cx.executor().run_until_parked();
19013 workspace
19014 .update(cx, |workspace, _, cx| {
19015 let active_item = workspace
19016 .active_item(cx)
19017 .expect("should have an active item after navigating back from the 2nd buffer");
19018 assert_eq!(
19019 active_item.item_id(),
19020 multibuffer_item_id,
19021 "Should navigate back from the 2nd buffer to the multi buffer"
19022 );
19023 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19024 })
19025 .unwrap();
19026
19027 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19028 editor.change_selections(
19029 SelectionEffects::scroll(Autoscroll::Next),
19030 window,
19031 cx,
19032 |s| s.select_ranges(Some(70..70)),
19033 );
19034 editor.open_excerpts(&OpenExcerpts, window, cx);
19035 });
19036 cx.executor().run_until_parked();
19037 workspace
19038 .update(cx, |workspace, window, cx| {
19039 let active_item = workspace
19040 .active_item(cx)
19041 .expect("should have an active item after navigating into the 3rd buffer");
19042 let third_item_id = active_item.item_id();
19043 assert_ne!(
19044 third_item_id, multibuffer_item_id,
19045 "Should navigate into the 3rd buffer and activate it"
19046 );
19047 assert_ne!(third_item_id, first_item_id);
19048 assert_ne!(third_item_id, second_item_id);
19049 assert_eq!(
19050 active_item.buffer_kind(cx),
19051 ItemBufferKind::Singleton,
19052 "New active item should be a singleton buffer"
19053 );
19054 assert_eq!(
19055 active_item
19056 .act_as::<Editor>(cx)
19057 .expect("should have navigated into an editor")
19058 .read(cx)
19059 .text(cx),
19060 sample_text_3
19061 );
19062
19063 workspace
19064 .go_back(workspace.active_pane().downgrade(), window, cx)
19065 .detach_and_log_err(cx);
19066 })
19067 .unwrap();
19068 cx.executor().run_until_parked();
19069 workspace
19070 .update(cx, |workspace, _, cx| {
19071 let active_item = workspace
19072 .active_item(cx)
19073 .expect("should have an active item after navigating back from the 3rd buffer");
19074 assert_eq!(
19075 active_item.item_id(),
19076 multibuffer_item_id,
19077 "Should navigate back from the 3rd buffer to the multi buffer"
19078 );
19079 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19080 })
19081 .unwrap();
19082}
19083
19084#[gpui::test]
19085async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19086 init_test(cx, |_| {});
19087
19088 let mut cx = EditorTestContext::new(cx).await;
19089
19090 let diff_base = r#"
19091 use some::mod;
19092
19093 const A: u32 = 42;
19094
19095 fn main() {
19096 println!("hello");
19097
19098 println!("world");
19099 }
19100 "#
19101 .unindent();
19102
19103 cx.set_state(
19104 &r#"
19105 use some::modified;
19106
19107 ˇ
19108 fn main() {
19109 println!("hello there");
19110
19111 println!("around the");
19112 println!("world");
19113 }
19114 "#
19115 .unindent(),
19116 );
19117
19118 cx.set_head_text(&diff_base);
19119 executor.run_until_parked();
19120
19121 cx.update_editor(|editor, window, cx| {
19122 editor.go_to_next_hunk(&GoToHunk, window, cx);
19123 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19124 });
19125 executor.run_until_parked();
19126 cx.assert_state_with_diff(
19127 r#"
19128 use some::modified;
19129
19130
19131 fn main() {
19132 - println!("hello");
19133 + ˇ println!("hello there");
19134
19135 println!("around the");
19136 println!("world");
19137 }
19138 "#
19139 .unindent(),
19140 );
19141
19142 cx.update_editor(|editor, window, cx| {
19143 for _ in 0..2 {
19144 editor.go_to_next_hunk(&GoToHunk, window, cx);
19145 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19146 }
19147 });
19148 executor.run_until_parked();
19149 cx.assert_state_with_diff(
19150 r#"
19151 - use some::mod;
19152 + ˇuse some::modified;
19153
19154
19155 fn main() {
19156 - println!("hello");
19157 + println!("hello there");
19158
19159 + println!("around the");
19160 println!("world");
19161 }
19162 "#
19163 .unindent(),
19164 );
19165
19166 cx.update_editor(|editor, window, cx| {
19167 editor.go_to_next_hunk(&GoToHunk, window, cx);
19168 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19169 });
19170 executor.run_until_parked();
19171 cx.assert_state_with_diff(
19172 r#"
19173 - use some::mod;
19174 + use some::modified;
19175
19176 - const A: u32 = 42;
19177 ˇ
19178 fn main() {
19179 - println!("hello");
19180 + println!("hello there");
19181
19182 + println!("around the");
19183 println!("world");
19184 }
19185 "#
19186 .unindent(),
19187 );
19188
19189 cx.update_editor(|editor, window, cx| {
19190 editor.cancel(&Cancel, window, cx);
19191 });
19192
19193 cx.assert_state_with_diff(
19194 r#"
19195 use some::modified;
19196
19197 ˇ
19198 fn main() {
19199 println!("hello there");
19200
19201 println!("around the");
19202 println!("world");
19203 }
19204 "#
19205 .unindent(),
19206 );
19207}
19208
19209#[gpui::test]
19210async fn test_diff_base_change_with_expanded_diff_hunks(
19211 executor: BackgroundExecutor,
19212 cx: &mut TestAppContext,
19213) {
19214 init_test(cx, |_| {});
19215
19216 let mut cx = EditorTestContext::new(cx).await;
19217
19218 let diff_base = r#"
19219 use some::mod1;
19220 use some::mod2;
19221
19222 const A: u32 = 42;
19223 const B: u32 = 42;
19224 const C: u32 = 42;
19225
19226 fn main() {
19227 println!("hello");
19228
19229 println!("world");
19230 }
19231 "#
19232 .unindent();
19233
19234 cx.set_state(
19235 &r#"
19236 use some::mod2;
19237
19238 const A: u32 = 42;
19239 const C: u32 = 42;
19240
19241 fn main(ˇ) {
19242 //println!("hello");
19243
19244 println!("world");
19245 //
19246 //
19247 }
19248 "#
19249 .unindent(),
19250 );
19251
19252 cx.set_head_text(&diff_base);
19253 executor.run_until_parked();
19254
19255 cx.update_editor(|editor, window, cx| {
19256 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19257 });
19258 executor.run_until_parked();
19259 cx.assert_state_with_diff(
19260 r#"
19261 - use some::mod1;
19262 use some::mod2;
19263
19264 const A: u32 = 42;
19265 - const B: u32 = 42;
19266 const C: u32 = 42;
19267
19268 fn main(ˇ) {
19269 - println!("hello");
19270 + //println!("hello");
19271
19272 println!("world");
19273 + //
19274 + //
19275 }
19276 "#
19277 .unindent(),
19278 );
19279
19280 cx.set_head_text("new diff base!");
19281 executor.run_until_parked();
19282 cx.assert_state_with_diff(
19283 r#"
19284 - new diff base!
19285 + use some::mod2;
19286 +
19287 + const A: u32 = 42;
19288 + const C: u32 = 42;
19289 +
19290 + fn main(ˇ) {
19291 + //println!("hello");
19292 +
19293 + println!("world");
19294 + //
19295 + //
19296 + }
19297 "#
19298 .unindent(),
19299 );
19300}
19301
19302#[gpui::test]
19303async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19304 init_test(cx, |_| {});
19305
19306 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19307 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19308 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19309 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19310 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19311 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19312
19313 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19314 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19315 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19316
19317 let multi_buffer = cx.new(|cx| {
19318 let mut multibuffer = MultiBuffer::new(ReadWrite);
19319 multibuffer.push_excerpts(
19320 buffer_1.clone(),
19321 [
19322 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19323 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19324 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19325 ],
19326 cx,
19327 );
19328 multibuffer.push_excerpts(
19329 buffer_2.clone(),
19330 [
19331 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19332 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19333 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19334 ],
19335 cx,
19336 );
19337 multibuffer.push_excerpts(
19338 buffer_3.clone(),
19339 [
19340 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19341 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19342 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19343 ],
19344 cx,
19345 );
19346 multibuffer
19347 });
19348
19349 let editor =
19350 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19351 editor
19352 .update(cx, |editor, _window, cx| {
19353 for (buffer, diff_base) in [
19354 (buffer_1.clone(), file_1_old),
19355 (buffer_2.clone(), file_2_old),
19356 (buffer_3.clone(), file_3_old),
19357 ] {
19358 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19359 editor
19360 .buffer
19361 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19362 }
19363 })
19364 .unwrap();
19365
19366 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19367 cx.run_until_parked();
19368
19369 cx.assert_editor_state(
19370 &"
19371 ˇaaa
19372 ccc
19373 ddd
19374
19375 ggg
19376 hhh
19377
19378
19379 lll
19380 mmm
19381 NNN
19382
19383 qqq
19384 rrr
19385
19386 uuu
19387 111
19388 222
19389 333
19390
19391 666
19392 777
19393
19394 000
19395 !!!"
19396 .unindent(),
19397 );
19398
19399 cx.update_editor(|editor, window, cx| {
19400 editor.select_all(&SelectAll, window, cx);
19401 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19402 });
19403 cx.executor().run_until_parked();
19404
19405 cx.assert_state_with_diff(
19406 "
19407 «aaa
19408 - bbb
19409 ccc
19410 ddd
19411
19412 ggg
19413 hhh
19414
19415
19416 lll
19417 mmm
19418 - nnn
19419 + NNN
19420
19421 qqq
19422 rrr
19423
19424 uuu
19425 111
19426 222
19427 333
19428
19429 + 666
19430 777
19431
19432 000
19433 !!!ˇ»"
19434 .unindent(),
19435 );
19436}
19437
19438#[gpui::test]
19439async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19440 init_test(cx, |_| {});
19441
19442 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19443 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19444
19445 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19446 let multi_buffer = cx.new(|cx| {
19447 let mut multibuffer = MultiBuffer::new(ReadWrite);
19448 multibuffer.push_excerpts(
19449 buffer.clone(),
19450 [
19451 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19452 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19453 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19454 ],
19455 cx,
19456 );
19457 multibuffer
19458 });
19459
19460 let editor =
19461 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19462 editor
19463 .update(cx, |editor, _window, cx| {
19464 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19465 editor
19466 .buffer
19467 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19468 })
19469 .unwrap();
19470
19471 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19472 cx.run_until_parked();
19473
19474 cx.update_editor(|editor, window, cx| {
19475 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19476 });
19477 cx.executor().run_until_parked();
19478
19479 // When the start of a hunk coincides with the start of its excerpt,
19480 // the hunk is expanded. When the start of a hunk is earlier than
19481 // the start of its excerpt, the hunk is not expanded.
19482 cx.assert_state_with_diff(
19483 "
19484 ˇaaa
19485 - bbb
19486 + BBB
19487
19488 - ddd
19489 - eee
19490 + DDD
19491 + EEE
19492 fff
19493
19494 iii
19495 "
19496 .unindent(),
19497 );
19498}
19499
19500#[gpui::test]
19501async fn test_edits_around_expanded_insertion_hunks(
19502 executor: BackgroundExecutor,
19503 cx: &mut TestAppContext,
19504) {
19505 init_test(cx, |_| {});
19506
19507 let mut cx = EditorTestContext::new(cx).await;
19508
19509 let diff_base = r#"
19510 use some::mod1;
19511 use some::mod2;
19512
19513 const A: u32 = 42;
19514
19515 fn main() {
19516 println!("hello");
19517
19518 println!("world");
19519 }
19520 "#
19521 .unindent();
19522 executor.run_until_parked();
19523 cx.set_state(
19524 &r#"
19525 use some::mod1;
19526 use some::mod2;
19527
19528 const A: u32 = 42;
19529 const B: u32 = 42;
19530 const C: u32 = 42;
19531 ˇ
19532
19533 fn main() {
19534 println!("hello");
19535
19536 println!("world");
19537 }
19538 "#
19539 .unindent(),
19540 );
19541
19542 cx.set_head_text(&diff_base);
19543 executor.run_until_parked();
19544
19545 cx.update_editor(|editor, window, cx| {
19546 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19547 });
19548 executor.run_until_parked();
19549
19550 cx.assert_state_with_diff(
19551 r#"
19552 use some::mod1;
19553 use some::mod2;
19554
19555 const A: u32 = 42;
19556 + const B: u32 = 42;
19557 + const C: u32 = 42;
19558 + ˇ
19559
19560 fn main() {
19561 println!("hello");
19562
19563 println!("world");
19564 }
19565 "#
19566 .unindent(),
19567 );
19568
19569 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19570 executor.run_until_parked();
19571
19572 cx.assert_state_with_diff(
19573 r#"
19574 use some::mod1;
19575 use some::mod2;
19576
19577 const A: u32 = 42;
19578 + const B: u32 = 42;
19579 + const C: u32 = 42;
19580 + const D: u32 = 42;
19581 + ˇ
19582
19583 fn main() {
19584 println!("hello");
19585
19586 println!("world");
19587 }
19588 "#
19589 .unindent(),
19590 );
19591
19592 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19593 executor.run_until_parked();
19594
19595 cx.assert_state_with_diff(
19596 r#"
19597 use some::mod1;
19598 use some::mod2;
19599
19600 const A: u32 = 42;
19601 + const B: u32 = 42;
19602 + const C: u32 = 42;
19603 + const D: u32 = 42;
19604 + const E: u32 = 42;
19605 + ˇ
19606
19607 fn main() {
19608 println!("hello");
19609
19610 println!("world");
19611 }
19612 "#
19613 .unindent(),
19614 );
19615
19616 cx.update_editor(|editor, window, cx| {
19617 editor.delete_line(&DeleteLine, window, cx);
19618 });
19619 executor.run_until_parked();
19620
19621 cx.assert_state_with_diff(
19622 r#"
19623 use some::mod1;
19624 use some::mod2;
19625
19626 const A: u32 = 42;
19627 + const B: u32 = 42;
19628 + const C: u32 = 42;
19629 + const D: u32 = 42;
19630 + const E: u32 = 42;
19631 ˇ
19632 fn main() {
19633 println!("hello");
19634
19635 println!("world");
19636 }
19637 "#
19638 .unindent(),
19639 );
19640
19641 cx.update_editor(|editor, window, cx| {
19642 editor.move_up(&MoveUp, window, cx);
19643 editor.delete_line(&DeleteLine, window, cx);
19644 editor.move_up(&MoveUp, window, cx);
19645 editor.delete_line(&DeleteLine, window, cx);
19646 editor.move_up(&MoveUp, window, cx);
19647 editor.delete_line(&DeleteLine, window, cx);
19648 });
19649 executor.run_until_parked();
19650 cx.assert_state_with_diff(
19651 r#"
19652 use some::mod1;
19653 use some::mod2;
19654
19655 const A: u32 = 42;
19656 + const B: u32 = 42;
19657 ˇ
19658 fn main() {
19659 println!("hello");
19660
19661 println!("world");
19662 }
19663 "#
19664 .unindent(),
19665 );
19666
19667 cx.update_editor(|editor, window, cx| {
19668 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19669 editor.delete_line(&DeleteLine, window, cx);
19670 });
19671 executor.run_until_parked();
19672 cx.assert_state_with_diff(
19673 r#"
19674 ˇ
19675 fn main() {
19676 println!("hello");
19677
19678 println!("world");
19679 }
19680 "#
19681 .unindent(),
19682 );
19683}
19684
19685#[gpui::test]
19686async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19687 init_test(cx, |_| {});
19688
19689 let mut cx = EditorTestContext::new(cx).await;
19690 cx.set_head_text(indoc! { "
19691 one
19692 two
19693 three
19694 four
19695 five
19696 "
19697 });
19698 cx.set_state(indoc! { "
19699 one
19700 ˇthree
19701 five
19702 "});
19703 cx.run_until_parked();
19704 cx.update_editor(|editor, window, cx| {
19705 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19706 });
19707 cx.assert_state_with_diff(
19708 indoc! { "
19709 one
19710 - two
19711 ˇthree
19712 - four
19713 five
19714 "}
19715 .to_string(),
19716 );
19717 cx.update_editor(|editor, window, cx| {
19718 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19719 });
19720
19721 cx.assert_state_with_diff(
19722 indoc! { "
19723 one
19724 ˇthree
19725 five
19726 "}
19727 .to_string(),
19728 );
19729
19730 cx.set_state(indoc! { "
19731 one
19732 ˇTWO
19733 three
19734 four
19735 five
19736 "});
19737 cx.run_until_parked();
19738 cx.update_editor(|editor, window, cx| {
19739 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19740 });
19741
19742 cx.assert_state_with_diff(
19743 indoc! { "
19744 one
19745 - two
19746 + ˇTWO
19747 three
19748 four
19749 five
19750 "}
19751 .to_string(),
19752 );
19753 cx.update_editor(|editor, window, cx| {
19754 editor.move_up(&Default::default(), window, cx);
19755 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19756 });
19757 cx.assert_state_with_diff(
19758 indoc! { "
19759 one
19760 ˇTWO
19761 three
19762 four
19763 five
19764 "}
19765 .to_string(),
19766 );
19767}
19768
19769#[gpui::test]
19770async fn test_edits_around_expanded_deletion_hunks(
19771 executor: BackgroundExecutor,
19772 cx: &mut TestAppContext,
19773) {
19774 init_test(cx, |_| {});
19775
19776 let mut cx = EditorTestContext::new(cx).await;
19777
19778 let diff_base = r#"
19779 use some::mod1;
19780 use some::mod2;
19781
19782 const A: u32 = 42;
19783 const B: u32 = 42;
19784 const C: u32 = 42;
19785
19786
19787 fn main() {
19788 println!("hello");
19789
19790 println!("world");
19791 }
19792 "#
19793 .unindent();
19794 executor.run_until_parked();
19795 cx.set_state(
19796 &r#"
19797 use some::mod1;
19798 use some::mod2;
19799
19800 ˇconst B: u32 = 42;
19801 const C: u32 = 42;
19802
19803
19804 fn main() {
19805 println!("hello");
19806
19807 println!("world");
19808 }
19809 "#
19810 .unindent(),
19811 );
19812
19813 cx.set_head_text(&diff_base);
19814 executor.run_until_parked();
19815
19816 cx.update_editor(|editor, window, cx| {
19817 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19818 });
19819 executor.run_until_parked();
19820
19821 cx.assert_state_with_diff(
19822 r#"
19823 use some::mod1;
19824 use some::mod2;
19825
19826 - const A: u32 = 42;
19827 ˇconst B: u32 = 42;
19828 const C: u32 = 42;
19829
19830
19831 fn main() {
19832 println!("hello");
19833
19834 println!("world");
19835 }
19836 "#
19837 .unindent(),
19838 );
19839
19840 cx.update_editor(|editor, window, cx| {
19841 editor.delete_line(&DeleteLine, window, cx);
19842 });
19843 executor.run_until_parked();
19844 cx.assert_state_with_diff(
19845 r#"
19846 use some::mod1;
19847 use some::mod2;
19848
19849 - const A: u32 = 42;
19850 - const B: u32 = 42;
19851 ˇconst C: u32 = 42;
19852
19853
19854 fn main() {
19855 println!("hello");
19856
19857 println!("world");
19858 }
19859 "#
19860 .unindent(),
19861 );
19862
19863 cx.update_editor(|editor, window, cx| {
19864 editor.delete_line(&DeleteLine, window, cx);
19865 });
19866 executor.run_until_parked();
19867 cx.assert_state_with_diff(
19868 r#"
19869 use some::mod1;
19870 use some::mod2;
19871
19872 - const A: u32 = 42;
19873 - const B: u32 = 42;
19874 - const C: u32 = 42;
19875 ˇ
19876
19877 fn main() {
19878 println!("hello");
19879
19880 println!("world");
19881 }
19882 "#
19883 .unindent(),
19884 );
19885
19886 cx.update_editor(|editor, window, cx| {
19887 editor.handle_input("replacement", window, cx);
19888 });
19889 executor.run_until_parked();
19890 cx.assert_state_with_diff(
19891 r#"
19892 use some::mod1;
19893 use some::mod2;
19894
19895 - const A: u32 = 42;
19896 - const B: u32 = 42;
19897 - const C: u32 = 42;
19898 -
19899 + replacementˇ
19900
19901 fn main() {
19902 println!("hello");
19903
19904 println!("world");
19905 }
19906 "#
19907 .unindent(),
19908 );
19909}
19910
19911#[gpui::test]
19912async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19913 init_test(cx, |_| {});
19914
19915 let mut cx = EditorTestContext::new(cx).await;
19916
19917 let base_text = r#"
19918 one
19919 two
19920 three
19921 four
19922 five
19923 "#
19924 .unindent();
19925 executor.run_until_parked();
19926 cx.set_state(
19927 &r#"
19928 one
19929 two
19930 fˇour
19931 five
19932 "#
19933 .unindent(),
19934 );
19935
19936 cx.set_head_text(&base_text);
19937 executor.run_until_parked();
19938
19939 cx.update_editor(|editor, window, cx| {
19940 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19941 });
19942 executor.run_until_parked();
19943
19944 cx.assert_state_with_diff(
19945 r#"
19946 one
19947 two
19948 - three
19949 fˇour
19950 five
19951 "#
19952 .unindent(),
19953 );
19954
19955 cx.update_editor(|editor, window, cx| {
19956 editor.backspace(&Backspace, window, cx);
19957 editor.backspace(&Backspace, window, cx);
19958 });
19959 executor.run_until_parked();
19960 cx.assert_state_with_diff(
19961 r#"
19962 one
19963 two
19964 - threeˇ
19965 - four
19966 + our
19967 five
19968 "#
19969 .unindent(),
19970 );
19971}
19972
19973#[gpui::test]
19974async fn test_edit_after_expanded_modification_hunk(
19975 executor: BackgroundExecutor,
19976 cx: &mut TestAppContext,
19977) {
19978 init_test(cx, |_| {});
19979
19980 let mut cx = EditorTestContext::new(cx).await;
19981
19982 let diff_base = r#"
19983 use some::mod1;
19984 use some::mod2;
19985
19986 const A: u32 = 42;
19987 const B: u32 = 42;
19988 const C: u32 = 42;
19989 const D: u32 = 42;
19990
19991
19992 fn main() {
19993 println!("hello");
19994
19995 println!("world");
19996 }"#
19997 .unindent();
19998
19999 cx.set_state(
20000 &r#"
20001 use some::mod1;
20002 use some::mod2;
20003
20004 const A: u32 = 42;
20005 const B: u32 = 42;
20006 const C: u32 = 43ˇ
20007 const D: u32 = 42;
20008
20009
20010 fn main() {
20011 println!("hello");
20012
20013 println!("world");
20014 }"#
20015 .unindent(),
20016 );
20017
20018 cx.set_head_text(&diff_base);
20019 executor.run_until_parked();
20020 cx.update_editor(|editor, window, cx| {
20021 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20022 });
20023 executor.run_until_parked();
20024
20025 cx.assert_state_with_diff(
20026 r#"
20027 use some::mod1;
20028 use some::mod2;
20029
20030 const A: u32 = 42;
20031 const B: u32 = 42;
20032 - const C: u32 = 42;
20033 + const C: u32 = 43ˇ
20034 const D: u32 = 42;
20035
20036
20037 fn main() {
20038 println!("hello");
20039
20040 println!("world");
20041 }"#
20042 .unindent(),
20043 );
20044
20045 cx.update_editor(|editor, window, cx| {
20046 editor.handle_input("\nnew_line\n", window, cx);
20047 });
20048 executor.run_until_parked();
20049
20050 cx.assert_state_with_diff(
20051 r#"
20052 use some::mod1;
20053 use some::mod2;
20054
20055 const A: u32 = 42;
20056 const B: u32 = 42;
20057 - const C: u32 = 42;
20058 + const C: u32 = 43
20059 + new_line
20060 + ˇ
20061 const D: u32 = 42;
20062
20063
20064 fn main() {
20065 println!("hello");
20066
20067 println!("world");
20068 }"#
20069 .unindent(),
20070 );
20071}
20072
20073#[gpui::test]
20074async fn test_stage_and_unstage_added_file_hunk(
20075 executor: BackgroundExecutor,
20076 cx: &mut TestAppContext,
20077) {
20078 init_test(cx, |_| {});
20079
20080 let mut cx = EditorTestContext::new(cx).await;
20081 cx.update_editor(|editor, _, cx| {
20082 editor.set_expand_all_diff_hunks(cx);
20083 });
20084
20085 let working_copy = r#"
20086 ˇfn main() {
20087 println!("hello, world!");
20088 }
20089 "#
20090 .unindent();
20091
20092 cx.set_state(&working_copy);
20093 executor.run_until_parked();
20094
20095 cx.assert_state_with_diff(
20096 r#"
20097 + ˇfn main() {
20098 + println!("hello, world!");
20099 + }
20100 "#
20101 .unindent(),
20102 );
20103 cx.assert_index_text(None);
20104
20105 cx.update_editor(|editor, window, cx| {
20106 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20107 });
20108 executor.run_until_parked();
20109 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20110 cx.assert_state_with_diff(
20111 r#"
20112 + ˇfn main() {
20113 + println!("hello, world!");
20114 + }
20115 "#
20116 .unindent(),
20117 );
20118
20119 cx.update_editor(|editor, window, cx| {
20120 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20121 });
20122 executor.run_until_parked();
20123 cx.assert_index_text(None);
20124}
20125
20126async fn setup_indent_guides_editor(
20127 text: &str,
20128 cx: &mut TestAppContext,
20129) -> (BufferId, EditorTestContext) {
20130 init_test(cx, |_| {});
20131
20132 let mut cx = EditorTestContext::new(cx).await;
20133
20134 let buffer_id = cx.update_editor(|editor, window, cx| {
20135 editor.set_text(text, window, cx);
20136 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20137
20138 buffer_ids[0]
20139 });
20140
20141 (buffer_id, cx)
20142}
20143
20144fn assert_indent_guides(
20145 range: Range<u32>,
20146 expected: Vec<IndentGuide>,
20147 active_indices: Option<Vec<usize>>,
20148 cx: &mut EditorTestContext,
20149) {
20150 let indent_guides = cx.update_editor(|editor, window, cx| {
20151 let snapshot = editor.snapshot(window, cx).display_snapshot;
20152 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20153 editor,
20154 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20155 true,
20156 &snapshot,
20157 cx,
20158 );
20159
20160 indent_guides.sort_by(|a, b| {
20161 a.depth.cmp(&b.depth).then(
20162 a.start_row
20163 .cmp(&b.start_row)
20164 .then(a.end_row.cmp(&b.end_row)),
20165 )
20166 });
20167 indent_guides
20168 });
20169
20170 if let Some(expected) = active_indices {
20171 let active_indices = cx.update_editor(|editor, window, cx| {
20172 let snapshot = editor.snapshot(window, cx).display_snapshot;
20173 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20174 });
20175
20176 assert_eq!(
20177 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20178 expected,
20179 "Active indent guide indices do not match"
20180 );
20181 }
20182
20183 assert_eq!(indent_guides, expected, "Indent guides do not match");
20184}
20185
20186fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20187 IndentGuide {
20188 buffer_id,
20189 start_row: MultiBufferRow(start_row),
20190 end_row: MultiBufferRow(end_row),
20191 depth,
20192 tab_size: 4,
20193 settings: IndentGuideSettings {
20194 enabled: true,
20195 line_width: 1,
20196 active_line_width: 1,
20197 coloring: IndentGuideColoring::default(),
20198 background_coloring: IndentGuideBackgroundColoring::default(),
20199 },
20200 }
20201}
20202
20203#[gpui::test]
20204async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20205 let (buffer_id, mut cx) = setup_indent_guides_editor(
20206 &"
20207 fn main() {
20208 let a = 1;
20209 }"
20210 .unindent(),
20211 cx,
20212 )
20213 .await;
20214
20215 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20216}
20217
20218#[gpui::test]
20219async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20220 let (buffer_id, mut cx) = setup_indent_guides_editor(
20221 &"
20222 fn main() {
20223 let a = 1;
20224 let b = 2;
20225 }"
20226 .unindent(),
20227 cx,
20228 )
20229 .await;
20230
20231 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20232}
20233
20234#[gpui::test]
20235async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20236 let (buffer_id, mut cx) = setup_indent_guides_editor(
20237 &"
20238 fn main() {
20239 let a = 1;
20240 if a == 3 {
20241 let b = 2;
20242 } else {
20243 let c = 3;
20244 }
20245 }"
20246 .unindent(),
20247 cx,
20248 )
20249 .await;
20250
20251 assert_indent_guides(
20252 0..8,
20253 vec![
20254 indent_guide(buffer_id, 1, 6, 0),
20255 indent_guide(buffer_id, 3, 3, 1),
20256 indent_guide(buffer_id, 5, 5, 1),
20257 ],
20258 None,
20259 &mut cx,
20260 );
20261}
20262
20263#[gpui::test]
20264async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20265 let (buffer_id, mut cx) = setup_indent_guides_editor(
20266 &"
20267 fn main() {
20268 let a = 1;
20269 let b = 2;
20270 let c = 3;
20271 }"
20272 .unindent(),
20273 cx,
20274 )
20275 .await;
20276
20277 assert_indent_guides(
20278 0..5,
20279 vec![
20280 indent_guide(buffer_id, 1, 3, 0),
20281 indent_guide(buffer_id, 2, 2, 1),
20282 ],
20283 None,
20284 &mut cx,
20285 );
20286}
20287
20288#[gpui::test]
20289async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20290 let (buffer_id, mut cx) = setup_indent_guides_editor(
20291 &"
20292 fn main() {
20293 let a = 1;
20294
20295 let c = 3;
20296 }"
20297 .unindent(),
20298 cx,
20299 )
20300 .await;
20301
20302 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20303}
20304
20305#[gpui::test]
20306async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20307 let (buffer_id, mut cx) = setup_indent_guides_editor(
20308 &"
20309 fn main() {
20310 let a = 1;
20311
20312 let c = 3;
20313
20314 if a == 3 {
20315 let b = 2;
20316 } else {
20317 let c = 3;
20318 }
20319 }"
20320 .unindent(),
20321 cx,
20322 )
20323 .await;
20324
20325 assert_indent_guides(
20326 0..11,
20327 vec![
20328 indent_guide(buffer_id, 1, 9, 0),
20329 indent_guide(buffer_id, 6, 6, 1),
20330 indent_guide(buffer_id, 8, 8, 1),
20331 ],
20332 None,
20333 &mut cx,
20334 );
20335}
20336
20337#[gpui::test]
20338async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20339 let (buffer_id, mut cx) = setup_indent_guides_editor(
20340 &"
20341 fn main() {
20342 let a = 1;
20343
20344 let c = 3;
20345
20346 if a == 3 {
20347 let b = 2;
20348 } else {
20349 let c = 3;
20350 }
20351 }"
20352 .unindent(),
20353 cx,
20354 )
20355 .await;
20356
20357 assert_indent_guides(
20358 1..11,
20359 vec![
20360 indent_guide(buffer_id, 1, 9, 0),
20361 indent_guide(buffer_id, 6, 6, 1),
20362 indent_guide(buffer_id, 8, 8, 1),
20363 ],
20364 None,
20365 &mut cx,
20366 );
20367}
20368
20369#[gpui::test]
20370async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20371 let (buffer_id, mut cx) = setup_indent_guides_editor(
20372 &"
20373 fn main() {
20374 let a = 1;
20375
20376 let c = 3;
20377
20378 if a == 3 {
20379 let b = 2;
20380 } else {
20381 let c = 3;
20382 }
20383 }"
20384 .unindent(),
20385 cx,
20386 )
20387 .await;
20388
20389 assert_indent_guides(
20390 1..10,
20391 vec![
20392 indent_guide(buffer_id, 1, 9, 0),
20393 indent_guide(buffer_id, 6, 6, 1),
20394 indent_guide(buffer_id, 8, 8, 1),
20395 ],
20396 None,
20397 &mut cx,
20398 );
20399}
20400
20401#[gpui::test]
20402async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20403 let (buffer_id, mut cx) = setup_indent_guides_editor(
20404 &"
20405 fn main() {
20406 if a {
20407 b(
20408 c,
20409 d,
20410 )
20411 } else {
20412 e(
20413 f
20414 )
20415 }
20416 }"
20417 .unindent(),
20418 cx,
20419 )
20420 .await;
20421
20422 assert_indent_guides(
20423 0..11,
20424 vec![
20425 indent_guide(buffer_id, 1, 10, 0),
20426 indent_guide(buffer_id, 2, 5, 1),
20427 indent_guide(buffer_id, 7, 9, 1),
20428 indent_guide(buffer_id, 3, 4, 2),
20429 indent_guide(buffer_id, 8, 8, 2),
20430 ],
20431 None,
20432 &mut cx,
20433 );
20434
20435 cx.update_editor(|editor, window, cx| {
20436 editor.fold_at(MultiBufferRow(2), window, cx);
20437 assert_eq!(
20438 editor.display_text(cx),
20439 "
20440 fn main() {
20441 if a {
20442 b(⋯
20443 )
20444 } else {
20445 e(
20446 f
20447 )
20448 }
20449 }"
20450 .unindent()
20451 );
20452 });
20453
20454 assert_indent_guides(
20455 0..11,
20456 vec![
20457 indent_guide(buffer_id, 1, 10, 0),
20458 indent_guide(buffer_id, 2, 5, 1),
20459 indent_guide(buffer_id, 7, 9, 1),
20460 indent_guide(buffer_id, 8, 8, 2),
20461 ],
20462 None,
20463 &mut cx,
20464 );
20465}
20466
20467#[gpui::test]
20468async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20469 let (buffer_id, mut cx) = setup_indent_guides_editor(
20470 &"
20471 block1
20472 block2
20473 block3
20474 block4
20475 block2
20476 block1
20477 block1"
20478 .unindent(),
20479 cx,
20480 )
20481 .await;
20482
20483 assert_indent_guides(
20484 1..10,
20485 vec![
20486 indent_guide(buffer_id, 1, 4, 0),
20487 indent_guide(buffer_id, 2, 3, 1),
20488 indent_guide(buffer_id, 3, 3, 2),
20489 ],
20490 None,
20491 &mut cx,
20492 );
20493}
20494
20495#[gpui::test]
20496async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20497 let (buffer_id, mut cx) = setup_indent_guides_editor(
20498 &"
20499 block1
20500 block2
20501 block3
20502
20503 block1
20504 block1"
20505 .unindent(),
20506 cx,
20507 )
20508 .await;
20509
20510 assert_indent_guides(
20511 0..6,
20512 vec![
20513 indent_guide(buffer_id, 1, 2, 0),
20514 indent_guide(buffer_id, 2, 2, 1),
20515 ],
20516 None,
20517 &mut cx,
20518 );
20519}
20520
20521#[gpui::test]
20522async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20523 let (buffer_id, mut cx) = setup_indent_guides_editor(
20524 &"
20525 function component() {
20526 \treturn (
20527 \t\t\t
20528 \t\t<div>
20529 \t\t\t<abc></abc>
20530 \t\t</div>
20531 \t)
20532 }"
20533 .unindent(),
20534 cx,
20535 )
20536 .await;
20537
20538 assert_indent_guides(
20539 0..8,
20540 vec![
20541 indent_guide(buffer_id, 1, 6, 0),
20542 indent_guide(buffer_id, 2, 5, 1),
20543 indent_guide(buffer_id, 4, 4, 2),
20544 ],
20545 None,
20546 &mut cx,
20547 );
20548}
20549
20550#[gpui::test]
20551async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20552 let (buffer_id, mut cx) = setup_indent_guides_editor(
20553 &"
20554 function component() {
20555 \treturn (
20556 \t
20557 \t\t<div>
20558 \t\t\t<abc></abc>
20559 \t\t</div>
20560 \t)
20561 }"
20562 .unindent(),
20563 cx,
20564 )
20565 .await;
20566
20567 assert_indent_guides(
20568 0..8,
20569 vec![
20570 indent_guide(buffer_id, 1, 6, 0),
20571 indent_guide(buffer_id, 2, 5, 1),
20572 indent_guide(buffer_id, 4, 4, 2),
20573 ],
20574 None,
20575 &mut cx,
20576 );
20577}
20578
20579#[gpui::test]
20580async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20581 let (buffer_id, mut cx) = setup_indent_guides_editor(
20582 &"
20583 block1
20584
20585
20586
20587 block2
20588 "
20589 .unindent(),
20590 cx,
20591 )
20592 .await;
20593
20594 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20595}
20596
20597#[gpui::test]
20598async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20599 let (buffer_id, mut cx) = setup_indent_guides_editor(
20600 &"
20601 def a:
20602 \tb = 3
20603 \tif True:
20604 \t\tc = 4
20605 \t\td = 5
20606 \tprint(b)
20607 "
20608 .unindent(),
20609 cx,
20610 )
20611 .await;
20612
20613 assert_indent_guides(
20614 0..6,
20615 vec![
20616 indent_guide(buffer_id, 1, 5, 0),
20617 indent_guide(buffer_id, 3, 4, 1),
20618 ],
20619 None,
20620 &mut cx,
20621 );
20622}
20623
20624#[gpui::test]
20625async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20626 let (buffer_id, mut cx) = setup_indent_guides_editor(
20627 &"
20628 fn main() {
20629 let a = 1;
20630 }"
20631 .unindent(),
20632 cx,
20633 )
20634 .await;
20635
20636 cx.update_editor(|editor, window, cx| {
20637 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20638 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20639 });
20640 });
20641
20642 assert_indent_guides(
20643 0..3,
20644 vec![indent_guide(buffer_id, 1, 1, 0)],
20645 Some(vec![0]),
20646 &mut cx,
20647 );
20648}
20649
20650#[gpui::test]
20651async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20652 let (buffer_id, mut cx) = setup_indent_guides_editor(
20653 &"
20654 fn main() {
20655 if 1 == 2 {
20656 let a = 1;
20657 }
20658 }"
20659 .unindent(),
20660 cx,
20661 )
20662 .await;
20663
20664 cx.update_editor(|editor, window, cx| {
20665 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20666 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20667 });
20668 });
20669
20670 assert_indent_guides(
20671 0..4,
20672 vec![
20673 indent_guide(buffer_id, 1, 3, 0),
20674 indent_guide(buffer_id, 2, 2, 1),
20675 ],
20676 Some(vec![1]),
20677 &mut cx,
20678 );
20679
20680 cx.update_editor(|editor, window, cx| {
20681 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20682 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20683 });
20684 });
20685
20686 assert_indent_guides(
20687 0..4,
20688 vec![
20689 indent_guide(buffer_id, 1, 3, 0),
20690 indent_guide(buffer_id, 2, 2, 1),
20691 ],
20692 Some(vec![1]),
20693 &mut cx,
20694 );
20695
20696 cx.update_editor(|editor, window, cx| {
20697 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20698 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20699 });
20700 });
20701
20702 assert_indent_guides(
20703 0..4,
20704 vec![
20705 indent_guide(buffer_id, 1, 3, 0),
20706 indent_guide(buffer_id, 2, 2, 1),
20707 ],
20708 Some(vec![0]),
20709 &mut cx,
20710 );
20711}
20712
20713#[gpui::test]
20714async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20715 let (buffer_id, mut cx) = setup_indent_guides_editor(
20716 &"
20717 fn main() {
20718 let a = 1;
20719
20720 let b = 2;
20721 }"
20722 .unindent(),
20723 cx,
20724 )
20725 .await;
20726
20727 cx.update_editor(|editor, window, cx| {
20728 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20729 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20730 });
20731 });
20732
20733 assert_indent_guides(
20734 0..5,
20735 vec![indent_guide(buffer_id, 1, 3, 0)],
20736 Some(vec![0]),
20737 &mut cx,
20738 );
20739}
20740
20741#[gpui::test]
20742async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20743 let (buffer_id, mut cx) = setup_indent_guides_editor(
20744 &"
20745 def m:
20746 a = 1
20747 pass"
20748 .unindent(),
20749 cx,
20750 )
20751 .await;
20752
20753 cx.update_editor(|editor, window, cx| {
20754 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20755 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20756 });
20757 });
20758
20759 assert_indent_guides(
20760 0..3,
20761 vec![indent_guide(buffer_id, 1, 2, 0)],
20762 Some(vec![0]),
20763 &mut cx,
20764 );
20765}
20766
20767#[gpui::test]
20768async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20769 init_test(cx, |_| {});
20770 let mut cx = EditorTestContext::new(cx).await;
20771 let text = indoc! {
20772 "
20773 impl A {
20774 fn b() {
20775 0;
20776 3;
20777 5;
20778 6;
20779 7;
20780 }
20781 }
20782 "
20783 };
20784 let base_text = indoc! {
20785 "
20786 impl A {
20787 fn b() {
20788 0;
20789 1;
20790 2;
20791 3;
20792 4;
20793 }
20794 fn c() {
20795 5;
20796 6;
20797 7;
20798 }
20799 }
20800 "
20801 };
20802
20803 cx.update_editor(|editor, window, cx| {
20804 editor.set_text(text, window, cx);
20805
20806 editor.buffer().update(cx, |multibuffer, cx| {
20807 let buffer = multibuffer.as_singleton().unwrap();
20808 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20809
20810 multibuffer.set_all_diff_hunks_expanded(cx);
20811 multibuffer.add_diff(diff, cx);
20812
20813 buffer.read(cx).remote_id()
20814 })
20815 });
20816 cx.run_until_parked();
20817
20818 cx.assert_state_with_diff(
20819 indoc! { "
20820 impl A {
20821 fn b() {
20822 0;
20823 - 1;
20824 - 2;
20825 3;
20826 - 4;
20827 - }
20828 - fn c() {
20829 5;
20830 6;
20831 7;
20832 }
20833 }
20834 ˇ"
20835 }
20836 .to_string(),
20837 );
20838
20839 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20840 editor
20841 .snapshot(window, cx)
20842 .buffer_snapshot()
20843 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20844 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20845 .collect::<Vec<_>>()
20846 });
20847 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20848 assert_eq!(
20849 actual_guides,
20850 vec![
20851 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20852 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20853 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20854 ]
20855 );
20856}
20857
20858#[gpui::test]
20859async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20860 init_test(cx, |_| {});
20861 let mut cx = EditorTestContext::new(cx).await;
20862
20863 let diff_base = r#"
20864 a
20865 b
20866 c
20867 "#
20868 .unindent();
20869
20870 cx.set_state(
20871 &r#"
20872 ˇA
20873 b
20874 C
20875 "#
20876 .unindent(),
20877 );
20878 cx.set_head_text(&diff_base);
20879 cx.update_editor(|editor, window, cx| {
20880 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20881 });
20882 executor.run_until_parked();
20883
20884 let both_hunks_expanded = r#"
20885 - a
20886 + ˇA
20887 b
20888 - c
20889 + C
20890 "#
20891 .unindent();
20892
20893 cx.assert_state_with_diff(both_hunks_expanded.clone());
20894
20895 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20896 let snapshot = editor.snapshot(window, cx);
20897 let hunks = editor
20898 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
20899 .collect::<Vec<_>>();
20900 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20901 let buffer_id = hunks[0].buffer_id;
20902 hunks
20903 .into_iter()
20904 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20905 .collect::<Vec<_>>()
20906 });
20907 assert_eq!(hunk_ranges.len(), 2);
20908
20909 cx.update_editor(|editor, _, cx| {
20910 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20911 });
20912 executor.run_until_parked();
20913
20914 let second_hunk_expanded = r#"
20915 ˇA
20916 b
20917 - c
20918 + C
20919 "#
20920 .unindent();
20921
20922 cx.assert_state_with_diff(second_hunk_expanded);
20923
20924 cx.update_editor(|editor, _, cx| {
20925 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20926 });
20927 executor.run_until_parked();
20928
20929 cx.assert_state_with_diff(both_hunks_expanded.clone());
20930
20931 cx.update_editor(|editor, _, cx| {
20932 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20933 });
20934 executor.run_until_parked();
20935
20936 let first_hunk_expanded = r#"
20937 - a
20938 + ˇA
20939 b
20940 C
20941 "#
20942 .unindent();
20943
20944 cx.assert_state_with_diff(first_hunk_expanded);
20945
20946 cx.update_editor(|editor, _, cx| {
20947 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20948 });
20949 executor.run_until_parked();
20950
20951 cx.assert_state_with_diff(both_hunks_expanded);
20952
20953 cx.set_state(
20954 &r#"
20955 ˇA
20956 b
20957 "#
20958 .unindent(),
20959 );
20960 cx.run_until_parked();
20961
20962 // TODO this cursor position seems bad
20963 cx.assert_state_with_diff(
20964 r#"
20965 - ˇa
20966 + A
20967 b
20968 "#
20969 .unindent(),
20970 );
20971
20972 cx.update_editor(|editor, window, cx| {
20973 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20974 });
20975
20976 cx.assert_state_with_diff(
20977 r#"
20978 - ˇa
20979 + A
20980 b
20981 - c
20982 "#
20983 .unindent(),
20984 );
20985
20986 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20987 let snapshot = editor.snapshot(window, cx);
20988 let hunks = editor
20989 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
20990 .collect::<Vec<_>>();
20991 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20992 let buffer_id = hunks[0].buffer_id;
20993 hunks
20994 .into_iter()
20995 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20996 .collect::<Vec<_>>()
20997 });
20998 assert_eq!(hunk_ranges.len(), 2);
20999
21000 cx.update_editor(|editor, _, cx| {
21001 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21002 });
21003 executor.run_until_parked();
21004
21005 cx.assert_state_with_diff(
21006 r#"
21007 - ˇa
21008 + A
21009 b
21010 "#
21011 .unindent(),
21012 );
21013}
21014
21015#[gpui::test]
21016async fn test_toggle_deletion_hunk_at_start_of_file(
21017 executor: BackgroundExecutor,
21018 cx: &mut TestAppContext,
21019) {
21020 init_test(cx, |_| {});
21021 let mut cx = EditorTestContext::new(cx).await;
21022
21023 let diff_base = r#"
21024 a
21025 b
21026 c
21027 "#
21028 .unindent();
21029
21030 cx.set_state(
21031 &r#"
21032 ˇb
21033 c
21034 "#
21035 .unindent(),
21036 );
21037 cx.set_head_text(&diff_base);
21038 cx.update_editor(|editor, window, cx| {
21039 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21040 });
21041 executor.run_until_parked();
21042
21043 let hunk_expanded = r#"
21044 - a
21045 ˇb
21046 c
21047 "#
21048 .unindent();
21049
21050 cx.assert_state_with_diff(hunk_expanded.clone());
21051
21052 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21053 let snapshot = editor.snapshot(window, cx);
21054 let hunks = editor
21055 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21056 .collect::<Vec<_>>();
21057 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21058 let buffer_id = hunks[0].buffer_id;
21059 hunks
21060 .into_iter()
21061 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21062 .collect::<Vec<_>>()
21063 });
21064 assert_eq!(hunk_ranges.len(), 1);
21065
21066 cx.update_editor(|editor, _, cx| {
21067 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21068 });
21069 executor.run_until_parked();
21070
21071 let hunk_collapsed = r#"
21072 ˇb
21073 c
21074 "#
21075 .unindent();
21076
21077 cx.assert_state_with_diff(hunk_collapsed);
21078
21079 cx.update_editor(|editor, _, cx| {
21080 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21081 });
21082 executor.run_until_parked();
21083
21084 cx.assert_state_with_diff(hunk_expanded);
21085}
21086
21087#[gpui::test]
21088async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21089 init_test(cx, |_| {});
21090
21091 let fs = FakeFs::new(cx.executor());
21092 fs.insert_tree(
21093 path!("/test"),
21094 json!({
21095 ".git": {},
21096 "file-1": "ONE\n",
21097 "file-2": "TWO\n",
21098 "file-3": "THREE\n",
21099 }),
21100 )
21101 .await;
21102
21103 fs.set_head_for_repo(
21104 path!("/test/.git").as_ref(),
21105 &[
21106 ("file-1", "one\n".into()),
21107 ("file-2", "two\n".into()),
21108 ("file-3", "three\n".into()),
21109 ],
21110 "deadbeef",
21111 );
21112
21113 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21114 let mut buffers = vec![];
21115 for i in 1..=3 {
21116 let buffer = project
21117 .update(cx, |project, cx| {
21118 let path = format!(path!("/test/file-{}"), i);
21119 project.open_local_buffer(path, cx)
21120 })
21121 .await
21122 .unwrap();
21123 buffers.push(buffer);
21124 }
21125
21126 let multibuffer = cx.new(|cx| {
21127 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21128 multibuffer.set_all_diff_hunks_expanded(cx);
21129 for buffer in &buffers {
21130 let snapshot = buffer.read(cx).snapshot();
21131 multibuffer.set_excerpts_for_path(
21132 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21133 buffer.clone(),
21134 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21135 2,
21136 cx,
21137 );
21138 }
21139 multibuffer
21140 });
21141
21142 let editor = cx.add_window(|window, cx| {
21143 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21144 });
21145 cx.run_until_parked();
21146
21147 let snapshot = editor
21148 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21149 .unwrap();
21150 let hunks = snapshot
21151 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21152 .map(|hunk| match hunk {
21153 DisplayDiffHunk::Unfolded {
21154 display_row_range, ..
21155 } => display_row_range,
21156 DisplayDiffHunk::Folded { .. } => unreachable!(),
21157 })
21158 .collect::<Vec<_>>();
21159 assert_eq!(
21160 hunks,
21161 [
21162 DisplayRow(2)..DisplayRow(4),
21163 DisplayRow(7)..DisplayRow(9),
21164 DisplayRow(12)..DisplayRow(14),
21165 ]
21166 );
21167}
21168
21169#[gpui::test]
21170async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21171 init_test(cx, |_| {});
21172
21173 let mut cx = EditorTestContext::new(cx).await;
21174 cx.set_head_text(indoc! { "
21175 one
21176 two
21177 three
21178 four
21179 five
21180 "
21181 });
21182 cx.set_index_text(indoc! { "
21183 one
21184 two
21185 three
21186 four
21187 five
21188 "
21189 });
21190 cx.set_state(indoc! {"
21191 one
21192 TWO
21193 ˇTHREE
21194 FOUR
21195 five
21196 "});
21197 cx.run_until_parked();
21198 cx.update_editor(|editor, window, cx| {
21199 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21200 });
21201 cx.run_until_parked();
21202 cx.assert_index_text(Some(indoc! {"
21203 one
21204 TWO
21205 THREE
21206 FOUR
21207 five
21208 "}));
21209 cx.set_state(indoc! { "
21210 one
21211 TWO
21212 ˇTHREE-HUNDRED
21213 FOUR
21214 five
21215 "});
21216 cx.run_until_parked();
21217 cx.update_editor(|editor, window, cx| {
21218 let snapshot = editor.snapshot(window, cx);
21219 let hunks = editor
21220 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21221 .collect::<Vec<_>>();
21222 assert_eq!(hunks.len(), 1);
21223 assert_eq!(
21224 hunks[0].status(),
21225 DiffHunkStatus {
21226 kind: DiffHunkStatusKind::Modified,
21227 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21228 }
21229 );
21230
21231 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21232 });
21233 cx.run_until_parked();
21234 cx.assert_index_text(Some(indoc! {"
21235 one
21236 TWO
21237 THREE-HUNDRED
21238 FOUR
21239 five
21240 "}));
21241}
21242
21243#[gpui::test]
21244fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21245 init_test(cx, |_| {});
21246
21247 let editor = cx.add_window(|window, cx| {
21248 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21249 build_editor(buffer, window, cx)
21250 });
21251
21252 let render_args = Arc::new(Mutex::new(None));
21253 let snapshot = editor
21254 .update(cx, |editor, window, cx| {
21255 let snapshot = editor.buffer().read(cx).snapshot(cx);
21256 let range =
21257 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21258
21259 struct RenderArgs {
21260 row: MultiBufferRow,
21261 folded: bool,
21262 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21263 }
21264
21265 let crease = Crease::inline(
21266 range,
21267 FoldPlaceholder::test(),
21268 {
21269 let toggle_callback = render_args.clone();
21270 move |row, folded, callback, _window, _cx| {
21271 *toggle_callback.lock() = Some(RenderArgs {
21272 row,
21273 folded,
21274 callback,
21275 });
21276 div()
21277 }
21278 },
21279 |_row, _folded, _window, _cx| div(),
21280 );
21281
21282 editor.insert_creases(Some(crease), cx);
21283 let snapshot = editor.snapshot(window, cx);
21284 let _div =
21285 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21286 snapshot
21287 })
21288 .unwrap();
21289
21290 let render_args = render_args.lock().take().unwrap();
21291 assert_eq!(render_args.row, MultiBufferRow(1));
21292 assert!(!render_args.folded);
21293 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21294
21295 cx.update_window(*editor, |_, window, cx| {
21296 (render_args.callback)(true, window, cx)
21297 })
21298 .unwrap();
21299 let snapshot = editor
21300 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21301 .unwrap();
21302 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21303
21304 cx.update_window(*editor, |_, window, cx| {
21305 (render_args.callback)(false, window, cx)
21306 })
21307 .unwrap();
21308 let snapshot = editor
21309 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21310 .unwrap();
21311 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21312}
21313
21314#[gpui::test]
21315async fn test_input_text(cx: &mut TestAppContext) {
21316 init_test(cx, |_| {});
21317 let mut cx = EditorTestContext::new(cx).await;
21318
21319 cx.set_state(
21320 &r#"ˇone
21321 two
21322
21323 three
21324 fourˇ
21325 five
21326
21327 siˇx"#
21328 .unindent(),
21329 );
21330
21331 cx.dispatch_action(HandleInput(String::new()));
21332 cx.assert_editor_state(
21333 &r#"ˇone
21334 two
21335
21336 three
21337 fourˇ
21338 five
21339
21340 siˇx"#
21341 .unindent(),
21342 );
21343
21344 cx.dispatch_action(HandleInput("AAAA".to_string()));
21345 cx.assert_editor_state(
21346 &r#"AAAAˇone
21347 two
21348
21349 three
21350 fourAAAAˇ
21351 five
21352
21353 siAAAAˇx"#
21354 .unindent(),
21355 );
21356}
21357
21358#[gpui::test]
21359async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21360 init_test(cx, |_| {});
21361
21362 let mut cx = EditorTestContext::new(cx).await;
21363 cx.set_state(
21364 r#"let foo = 1;
21365let foo = 2;
21366let foo = 3;
21367let fooˇ = 4;
21368let foo = 5;
21369let foo = 6;
21370let foo = 7;
21371let foo = 8;
21372let foo = 9;
21373let foo = 10;
21374let foo = 11;
21375let foo = 12;
21376let foo = 13;
21377let foo = 14;
21378let foo = 15;"#,
21379 );
21380
21381 cx.update_editor(|e, window, cx| {
21382 assert_eq!(
21383 e.next_scroll_position,
21384 NextScrollCursorCenterTopBottom::Center,
21385 "Default next scroll direction is center",
21386 );
21387
21388 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21389 assert_eq!(
21390 e.next_scroll_position,
21391 NextScrollCursorCenterTopBottom::Top,
21392 "After center, next scroll direction should be top",
21393 );
21394
21395 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21396 assert_eq!(
21397 e.next_scroll_position,
21398 NextScrollCursorCenterTopBottom::Bottom,
21399 "After top, next scroll direction should be bottom",
21400 );
21401
21402 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21403 assert_eq!(
21404 e.next_scroll_position,
21405 NextScrollCursorCenterTopBottom::Center,
21406 "After bottom, scrolling should start over",
21407 );
21408
21409 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21410 assert_eq!(
21411 e.next_scroll_position,
21412 NextScrollCursorCenterTopBottom::Top,
21413 "Scrolling continues if retriggered fast enough"
21414 );
21415 });
21416
21417 cx.executor()
21418 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21419 cx.executor().run_until_parked();
21420 cx.update_editor(|e, _, _| {
21421 assert_eq!(
21422 e.next_scroll_position,
21423 NextScrollCursorCenterTopBottom::Center,
21424 "If scrolling is not triggered fast enough, it should reset"
21425 );
21426 });
21427}
21428
21429#[gpui::test]
21430async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21431 init_test(cx, |_| {});
21432 let mut cx = EditorLspTestContext::new_rust(
21433 lsp::ServerCapabilities {
21434 definition_provider: Some(lsp::OneOf::Left(true)),
21435 references_provider: Some(lsp::OneOf::Left(true)),
21436 ..lsp::ServerCapabilities::default()
21437 },
21438 cx,
21439 )
21440 .await;
21441
21442 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21443 let go_to_definition = cx
21444 .lsp
21445 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21446 move |params, _| async move {
21447 if empty_go_to_definition {
21448 Ok(None)
21449 } else {
21450 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21451 uri: params.text_document_position_params.text_document.uri,
21452 range: lsp::Range::new(
21453 lsp::Position::new(4, 3),
21454 lsp::Position::new(4, 6),
21455 ),
21456 })))
21457 }
21458 },
21459 );
21460 let references = cx
21461 .lsp
21462 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21463 Ok(Some(vec![lsp::Location {
21464 uri: params.text_document_position.text_document.uri,
21465 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21466 }]))
21467 });
21468 (go_to_definition, references)
21469 };
21470
21471 cx.set_state(
21472 &r#"fn one() {
21473 let mut a = ˇtwo();
21474 }
21475
21476 fn two() {}"#
21477 .unindent(),
21478 );
21479 set_up_lsp_handlers(false, &mut cx);
21480 let navigated = cx
21481 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21482 .await
21483 .expect("Failed to navigate to definition");
21484 assert_eq!(
21485 navigated,
21486 Navigated::Yes,
21487 "Should have navigated to definition from the GetDefinition response"
21488 );
21489 cx.assert_editor_state(
21490 &r#"fn one() {
21491 let mut a = two();
21492 }
21493
21494 fn «twoˇ»() {}"#
21495 .unindent(),
21496 );
21497
21498 let editors = cx.update_workspace(|workspace, _, cx| {
21499 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21500 });
21501 cx.update_editor(|_, _, test_editor_cx| {
21502 assert_eq!(
21503 editors.len(),
21504 1,
21505 "Initially, only one, test, editor should be open in the workspace"
21506 );
21507 assert_eq!(
21508 test_editor_cx.entity(),
21509 editors.last().expect("Asserted len is 1").clone()
21510 );
21511 });
21512
21513 set_up_lsp_handlers(true, &mut cx);
21514 let navigated = cx
21515 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21516 .await
21517 .expect("Failed to navigate to lookup references");
21518 assert_eq!(
21519 navigated,
21520 Navigated::Yes,
21521 "Should have navigated to references as a fallback after empty GoToDefinition response"
21522 );
21523 // We should not change the selections in the existing file,
21524 // if opening another milti buffer with the references
21525 cx.assert_editor_state(
21526 &r#"fn one() {
21527 let mut a = two();
21528 }
21529
21530 fn «twoˇ»() {}"#
21531 .unindent(),
21532 );
21533 let editors = cx.update_workspace(|workspace, _, cx| {
21534 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21535 });
21536 cx.update_editor(|_, _, test_editor_cx| {
21537 assert_eq!(
21538 editors.len(),
21539 2,
21540 "After falling back to references search, we open a new editor with the results"
21541 );
21542 let references_fallback_text = editors
21543 .into_iter()
21544 .find(|new_editor| *new_editor != test_editor_cx.entity())
21545 .expect("Should have one non-test editor now")
21546 .read(test_editor_cx)
21547 .text(test_editor_cx);
21548 assert_eq!(
21549 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21550 "Should use the range from the references response and not the GoToDefinition one"
21551 );
21552 });
21553}
21554
21555#[gpui::test]
21556async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21557 init_test(cx, |_| {});
21558 cx.update(|cx| {
21559 let mut editor_settings = EditorSettings::get_global(cx).clone();
21560 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21561 EditorSettings::override_global(editor_settings, cx);
21562 });
21563 let mut cx = EditorLspTestContext::new_rust(
21564 lsp::ServerCapabilities {
21565 definition_provider: Some(lsp::OneOf::Left(true)),
21566 references_provider: Some(lsp::OneOf::Left(true)),
21567 ..lsp::ServerCapabilities::default()
21568 },
21569 cx,
21570 )
21571 .await;
21572 let original_state = r#"fn one() {
21573 let mut a = ˇtwo();
21574 }
21575
21576 fn two() {}"#
21577 .unindent();
21578 cx.set_state(&original_state);
21579
21580 let mut go_to_definition = cx
21581 .lsp
21582 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21583 move |_, _| async move { Ok(None) },
21584 );
21585 let _references = cx
21586 .lsp
21587 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21588 panic!("Should not call for references with no go to definition fallback")
21589 });
21590
21591 let navigated = cx
21592 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21593 .await
21594 .expect("Failed to navigate to lookup references");
21595 go_to_definition
21596 .next()
21597 .await
21598 .expect("Should have called the go_to_definition handler");
21599
21600 assert_eq!(
21601 navigated,
21602 Navigated::No,
21603 "Should have navigated to references as a fallback after empty GoToDefinition response"
21604 );
21605 cx.assert_editor_state(&original_state);
21606 let editors = cx.update_workspace(|workspace, _, cx| {
21607 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21608 });
21609 cx.update_editor(|_, _, _| {
21610 assert_eq!(
21611 editors.len(),
21612 1,
21613 "After unsuccessful fallback, no other editor should have been opened"
21614 );
21615 });
21616}
21617
21618#[gpui::test]
21619async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21620 init_test(cx, |_| {});
21621 let mut cx = EditorLspTestContext::new_rust(
21622 lsp::ServerCapabilities {
21623 references_provider: Some(lsp::OneOf::Left(true)),
21624 ..lsp::ServerCapabilities::default()
21625 },
21626 cx,
21627 )
21628 .await;
21629
21630 cx.set_state(
21631 &r#"
21632 fn one() {
21633 let mut a = two();
21634 }
21635
21636 fn ˇtwo() {}"#
21637 .unindent(),
21638 );
21639 cx.lsp
21640 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21641 Ok(Some(vec![
21642 lsp::Location {
21643 uri: params.text_document_position.text_document.uri.clone(),
21644 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21645 },
21646 lsp::Location {
21647 uri: params.text_document_position.text_document.uri,
21648 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21649 },
21650 ]))
21651 });
21652 let navigated = cx
21653 .update_editor(|editor, window, cx| {
21654 editor.find_all_references(&FindAllReferences, window, cx)
21655 })
21656 .unwrap()
21657 .await
21658 .expect("Failed to navigate to references");
21659 assert_eq!(
21660 navigated,
21661 Navigated::Yes,
21662 "Should have navigated to references from the FindAllReferences response"
21663 );
21664 cx.assert_editor_state(
21665 &r#"fn one() {
21666 let mut a = two();
21667 }
21668
21669 fn ˇtwo() {}"#
21670 .unindent(),
21671 );
21672
21673 let editors = cx.update_workspace(|workspace, _, cx| {
21674 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21675 });
21676 cx.update_editor(|_, _, _| {
21677 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21678 });
21679
21680 cx.set_state(
21681 &r#"fn one() {
21682 let mut a = ˇtwo();
21683 }
21684
21685 fn two() {}"#
21686 .unindent(),
21687 );
21688 let navigated = cx
21689 .update_editor(|editor, window, cx| {
21690 editor.find_all_references(&FindAllReferences, window, cx)
21691 })
21692 .unwrap()
21693 .await
21694 .expect("Failed to navigate to references");
21695 assert_eq!(
21696 navigated,
21697 Navigated::Yes,
21698 "Should have navigated to references from the FindAllReferences response"
21699 );
21700 cx.assert_editor_state(
21701 &r#"fn one() {
21702 let mut a = ˇtwo();
21703 }
21704
21705 fn two() {}"#
21706 .unindent(),
21707 );
21708 let editors = cx.update_workspace(|workspace, _, cx| {
21709 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21710 });
21711 cx.update_editor(|_, _, _| {
21712 assert_eq!(
21713 editors.len(),
21714 2,
21715 "should have re-used the previous multibuffer"
21716 );
21717 });
21718
21719 cx.set_state(
21720 &r#"fn one() {
21721 let mut a = ˇtwo();
21722 }
21723 fn three() {}
21724 fn two() {}"#
21725 .unindent(),
21726 );
21727 cx.lsp
21728 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21729 Ok(Some(vec![
21730 lsp::Location {
21731 uri: params.text_document_position.text_document.uri.clone(),
21732 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21733 },
21734 lsp::Location {
21735 uri: params.text_document_position.text_document.uri,
21736 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21737 },
21738 ]))
21739 });
21740 let navigated = cx
21741 .update_editor(|editor, window, cx| {
21742 editor.find_all_references(&FindAllReferences, window, cx)
21743 })
21744 .unwrap()
21745 .await
21746 .expect("Failed to navigate to references");
21747 assert_eq!(
21748 navigated,
21749 Navigated::Yes,
21750 "Should have navigated to references from the FindAllReferences response"
21751 );
21752 cx.assert_editor_state(
21753 &r#"fn one() {
21754 let mut a = ˇtwo();
21755 }
21756 fn three() {}
21757 fn two() {}"#
21758 .unindent(),
21759 );
21760 let editors = cx.update_workspace(|workspace, _, cx| {
21761 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21762 });
21763 cx.update_editor(|_, _, _| {
21764 assert_eq!(
21765 editors.len(),
21766 3,
21767 "should have used a new multibuffer as offsets changed"
21768 );
21769 });
21770}
21771#[gpui::test]
21772async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21773 init_test(cx, |_| {});
21774
21775 let language = Arc::new(Language::new(
21776 LanguageConfig::default(),
21777 Some(tree_sitter_rust::LANGUAGE.into()),
21778 ));
21779
21780 let text = r#"
21781 #[cfg(test)]
21782 mod tests() {
21783 #[test]
21784 fn runnable_1() {
21785 let a = 1;
21786 }
21787
21788 #[test]
21789 fn runnable_2() {
21790 let a = 1;
21791 let b = 2;
21792 }
21793 }
21794 "#
21795 .unindent();
21796
21797 let fs = FakeFs::new(cx.executor());
21798 fs.insert_file("/file.rs", Default::default()).await;
21799
21800 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21801 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21802 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21803 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21804 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21805
21806 let editor = cx.new_window_entity(|window, cx| {
21807 Editor::new(
21808 EditorMode::full(),
21809 multi_buffer,
21810 Some(project.clone()),
21811 window,
21812 cx,
21813 )
21814 });
21815
21816 editor.update_in(cx, |editor, window, cx| {
21817 let snapshot = editor.buffer().read(cx).snapshot(cx);
21818 editor.tasks.insert(
21819 (buffer.read(cx).remote_id(), 3),
21820 RunnableTasks {
21821 templates: vec![],
21822 offset: snapshot.anchor_before(43),
21823 column: 0,
21824 extra_variables: HashMap::default(),
21825 context_range: BufferOffset(43)..BufferOffset(85),
21826 },
21827 );
21828 editor.tasks.insert(
21829 (buffer.read(cx).remote_id(), 8),
21830 RunnableTasks {
21831 templates: vec![],
21832 offset: snapshot.anchor_before(86),
21833 column: 0,
21834 extra_variables: HashMap::default(),
21835 context_range: BufferOffset(86)..BufferOffset(191),
21836 },
21837 );
21838
21839 // Test finding task when cursor is inside function body
21840 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21841 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21842 });
21843 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21844 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21845
21846 // Test finding task when cursor is on function name
21847 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21848 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21849 });
21850 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21851 assert_eq!(row, 8, "Should find task when cursor is on function name");
21852 });
21853}
21854
21855#[gpui::test]
21856async fn test_folding_buffers(cx: &mut TestAppContext) {
21857 init_test(cx, |_| {});
21858
21859 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21860 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21861 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21862
21863 let fs = FakeFs::new(cx.executor());
21864 fs.insert_tree(
21865 path!("/a"),
21866 json!({
21867 "first.rs": sample_text_1,
21868 "second.rs": sample_text_2,
21869 "third.rs": sample_text_3,
21870 }),
21871 )
21872 .await;
21873 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21874 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21875 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21876 let worktree = project.update(cx, |project, cx| {
21877 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21878 assert_eq!(worktrees.len(), 1);
21879 worktrees.pop().unwrap()
21880 });
21881 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21882
21883 let buffer_1 = project
21884 .update(cx, |project, cx| {
21885 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
21886 })
21887 .await
21888 .unwrap();
21889 let buffer_2 = project
21890 .update(cx, |project, cx| {
21891 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
21892 })
21893 .await
21894 .unwrap();
21895 let buffer_3 = project
21896 .update(cx, |project, cx| {
21897 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
21898 })
21899 .await
21900 .unwrap();
21901
21902 let multi_buffer = cx.new(|cx| {
21903 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21904 multi_buffer.push_excerpts(
21905 buffer_1.clone(),
21906 [
21907 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21908 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21909 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21910 ],
21911 cx,
21912 );
21913 multi_buffer.push_excerpts(
21914 buffer_2.clone(),
21915 [
21916 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21917 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21918 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21919 ],
21920 cx,
21921 );
21922 multi_buffer.push_excerpts(
21923 buffer_3.clone(),
21924 [
21925 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21926 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21927 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21928 ],
21929 cx,
21930 );
21931 multi_buffer
21932 });
21933 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21934 Editor::new(
21935 EditorMode::full(),
21936 multi_buffer.clone(),
21937 Some(project.clone()),
21938 window,
21939 cx,
21940 )
21941 });
21942
21943 assert_eq!(
21944 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21945 "\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",
21946 );
21947
21948 multi_buffer_editor.update(cx, |editor, cx| {
21949 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21950 });
21951 assert_eq!(
21952 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21953 "\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",
21954 "After folding the first buffer, its text should not be displayed"
21955 );
21956
21957 multi_buffer_editor.update(cx, |editor, cx| {
21958 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21959 });
21960 assert_eq!(
21961 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21962 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21963 "After folding the second buffer, its text should not be displayed"
21964 );
21965
21966 multi_buffer_editor.update(cx, |editor, cx| {
21967 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21968 });
21969 assert_eq!(
21970 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21971 "\n\n\n\n\n",
21972 "After folding the third buffer, its text should not be displayed"
21973 );
21974
21975 // Emulate selection inside the fold logic, that should work
21976 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21977 editor
21978 .snapshot(window, cx)
21979 .next_line_boundary(Point::new(0, 4));
21980 });
21981
21982 multi_buffer_editor.update(cx, |editor, cx| {
21983 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21984 });
21985 assert_eq!(
21986 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21987 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21988 "After unfolding the second buffer, its text should be displayed"
21989 );
21990
21991 // Typing inside of buffer 1 causes that buffer to be unfolded.
21992 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21993 assert_eq!(
21994 multi_buffer
21995 .read(cx)
21996 .snapshot(cx)
21997 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21998 .collect::<String>(),
21999 "bbbb"
22000 );
22001 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22002 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22003 });
22004 editor.handle_input("B", window, cx);
22005 });
22006
22007 assert_eq!(
22008 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22009 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22010 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22011 );
22012
22013 multi_buffer_editor.update(cx, |editor, cx| {
22014 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22015 });
22016 assert_eq!(
22017 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22018 "\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",
22019 "After unfolding the all buffers, all original text should be displayed"
22020 );
22021}
22022
22023#[gpui::test]
22024async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22025 init_test(cx, |_| {});
22026
22027 let sample_text_1 = "1111\n2222\n3333".to_string();
22028 let sample_text_2 = "4444\n5555\n6666".to_string();
22029 let sample_text_3 = "7777\n8888\n9999".to_string();
22030
22031 let fs = FakeFs::new(cx.executor());
22032 fs.insert_tree(
22033 path!("/a"),
22034 json!({
22035 "first.rs": sample_text_1,
22036 "second.rs": sample_text_2,
22037 "third.rs": sample_text_3,
22038 }),
22039 )
22040 .await;
22041 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22042 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22043 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22044 let worktree = project.update(cx, |project, cx| {
22045 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22046 assert_eq!(worktrees.len(), 1);
22047 worktrees.pop().unwrap()
22048 });
22049 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22050
22051 let buffer_1 = project
22052 .update(cx, |project, cx| {
22053 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22054 })
22055 .await
22056 .unwrap();
22057 let buffer_2 = project
22058 .update(cx, |project, cx| {
22059 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22060 })
22061 .await
22062 .unwrap();
22063 let buffer_3 = project
22064 .update(cx, |project, cx| {
22065 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22066 })
22067 .await
22068 .unwrap();
22069
22070 let multi_buffer = cx.new(|cx| {
22071 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22072 multi_buffer.push_excerpts(
22073 buffer_1.clone(),
22074 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22075 cx,
22076 );
22077 multi_buffer.push_excerpts(
22078 buffer_2.clone(),
22079 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22080 cx,
22081 );
22082 multi_buffer.push_excerpts(
22083 buffer_3.clone(),
22084 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22085 cx,
22086 );
22087 multi_buffer
22088 });
22089
22090 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22091 Editor::new(
22092 EditorMode::full(),
22093 multi_buffer,
22094 Some(project.clone()),
22095 window,
22096 cx,
22097 )
22098 });
22099
22100 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22101 assert_eq!(
22102 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22103 full_text,
22104 );
22105
22106 multi_buffer_editor.update(cx, |editor, cx| {
22107 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22108 });
22109 assert_eq!(
22110 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22111 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22112 "After folding the first buffer, its text should not be displayed"
22113 );
22114
22115 multi_buffer_editor.update(cx, |editor, cx| {
22116 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22117 });
22118
22119 assert_eq!(
22120 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22121 "\n\n\n\n\n\n7777\n8888\n9999",
22122 "After folding the second buffer, its text should not be displayed"
22123 );
22124
22125 multi_buffer_editor.update(cx, |editor, cx| {
22126 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22127 });
22128 assert_eq!(
22129 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22130 "\n\n\n\n\n",
22131 "After folding the third buffer, its text should not be displayed"
22132 );
22133
22134 multi_buffer_editor.update(cx, |editor, cx| {
22135 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22136 });
22137 assert_eq!(
22138 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22139 "\n\n\n\n4444\n5555\n6666\n\n",
22140 "After unfolding the second buffer, its text should be displayed"
22141 );
22142
22143 multi_buffer_editor.update(cx, |editor, cx| {
22144 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22145 });
22146 assert_eq!(
22147 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22148 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22149 "After unfolding the first buffer, its text should be displayed"
22150 );
22151
22152 multi_buffer_editor.update(cx, |editor, cx| {
22153 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22154 });
22155 assert_eq!(
22156 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22157 full_text,
22158 "After unfolding all buffers, all original text should be displayed"
22159 );
22160}
22161
22162#[gpui::test]
22163async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22164 init_test(cx, |_| {});
22165
22166 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22167
22168 let fs = FakeFs::new(cx.executor());
22169 fs.insert_tree(
22170 path!("/a"),
22171 json!({
22172 "main.rs": sample_text,
22173 }),
22174 )
22175 .await;
22176 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22177 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22178 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22179 let worktree = project.update(cx, |project, cx| {
22180 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22181 assert_eq!(worktrees.len(), 1);
22182 worktrees.pop().unwrap()
22183 });
22184 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22185
22186 let buffer_1 = project
22187 .update(cx, |project, cx| {
22188 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22189 })
22190 .await
22191 .unwrap();
22192
22193 let multi_buffer = cx.new(|cx| {
22194 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22195 multi_buffer.push_excerpts(
22196 buffer_1.clone(),
22197 [ExcerptRange::new(
22198 Point::new(0, 0)
22199 ..Point::new(
22200 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22201 0,
22202 ),
22203 )],
22204 cx,
22205 );
22206 multi_buffer
22207 });
22208 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22209 Editor::new(
22210 EditorMode::full(),
22211 multi_buffer,
22212 Some(project.clone()),
22213 window,
22214 cx,
22215 )
22216 });
22217
22218 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22219 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22220 enum TestHighlight {}
22221 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22222 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22223 editor.highlight_text::<TestHighlight>(
22224 vec![highlight_range.clone()],
22225 HighlightStyle::color(Hsla::green()),
22226 cx,
22227 );
22228 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22229 s.select_ranges(Some(highlight_range))
22230 });
22231 });
22232
22233 let full_text = format!("\n\n{sample_text}");
22234 assert_eq!(
22235 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22236 full_text,
22237 );
22238}
22239
22240#[gpui::test]
22241async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22242 init_test(cx, |_| {});
22243 cx.update(|cx| {
22244 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22245 "keymaps/default-linux.json",
22246 cx,
22247 )
22248 .unwrap();
22249 cx.bind_keys(default_key_bindings);
22250 });
22251
22252 let (editor, cx) = cx.add_window_view(|window, cx| {
22253 let multi_buffer = MultiBuffer::build_multi(
22254 [
22255 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22256 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22257 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22258 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22259 ],
22260 cx,
22261 );
22262 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22263
22264 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22265 // fold all but the second buffer, so that we test navigating between two
22266 // adjacent folded buffers, as well as folded buffers at the start and
22267 // end the multibuffer
22268 editor.fold_buffer(buffer_ids[0], cx);
22269 editor.fold_buffer(buffer_ids[2], cx);
22270 editor.fold_buffer(buffer_ids[3], cx);
22271
22272 editor
22273 });
22274 cx.simulate_resize(size(px(1000.), px(1000.)));
22275
22276 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22277 cx.assert_excerpts_with_selections(indoc! {"
22278 [EXCERPT]
22279 ˇ[FOLDED]
22280 [EXCERPT]
22281 a1
22282 b1
22283 [EXCERPT]
22284 [FOLDED]
22285 [EXCERPT]
22286 [FOLDED]
22287 "
22288 });
22289 cx.simulate_keystroke("down");
22290 cx.assert_excerpts_with_selections(indoc! {"
22291 [EXCERPT]
22292 [FOLDED]
22293 [EXCERPT]
22294 ˇa1
22295 b1
22296 [EXCERPT]
22297 [FOLDED]
22298 [EXCERPT]
22299 [FOLDED]
22300 "
22301 });
22302 cx.simulate_keystroke("down");
22303 cx.assert_excerpts_with_selections(indoc! {"
22304 [EXCERPT]
22305 [FOLDED]
22306 [EXCERPT]
22307 a1
22308 ˇb1
22309 [EXCERPT]
22310 [FOLDED]
22311 [EXCERPT]
22312 [FOLDED]
22313 "
22314 });
22315 cx.simulate_keystroke("down");
22316 cx.assert_excerpts_with_selections(indoc! {"
22317 [EXCERPT]
22318 [FOLDED]
22319 [EXCERPT]
22320 a1
22321 b1
22322 ˇ[EXCERPT]
22323 [FOLDED]
22324 [EXCERPT]
22325 [FOLDED]
22326 "
22327 });
22328 cx.simulate_keystroke("down");
22329 cx.assert_excerpts_with_selections(indoc! {"
22330 [EXCERPT]
22331 [FOLDED]
22332 [EXCERPT]
22333 a1
22334 b1
22335 [EXCERPT]
22336 ˇ[FOLDED]
22337 [EXCERPT]
22338 [FOLDED]
22339 "
22340 });
22341 for _ in 0..5 {
22342 cx.simulate_keystroke("down");
22343 cx.assert_excerpts_with_selections(indoc! {"
22344 [EXCERPT]
22345 [FOLDED]
22346 [EXCERPT]
22347 a1
22348 b1
22349 [EXCERPT]
22350 [FOLDED]
22351 [EXCERPT]
22352 ˇ[FOLDED]
22353 "
22354 });
22355 }
22356
22357 cx.simulate_keystroke("up");
22358 cx.assert_excerpts_with_selections(indoc! {"
22359 [EXCERPT]
22360 [FOLDED]
22361 [EXCERPT]
22362 a1
22363 b1
22364 [EXCERPT]
22365 ˇ[FOLDED]
22366 [EXCERPT]
22367 [FOLDED]
22368 "
22369 });
22370 cx.simulate_keystroke("up");
22371 cx.assert_excerpts_with_selections(indoc! {"
22372 [EXCERPT]
22373 [FOLDED]
22374 [EXCERPT]
22375 a1
22376 b1
22377 ˇ[EXCERPT]
22378 [FOLDED]
22379 [EXCERPT]
22380 [FOLDED]
22381 "
22382 });
22383 cx.simulate_keystroke("up");
22384 cx.assert_excerpts_with_selections(indoc! {"
22385 [EXCERPT]
22386 [FOLDED]
22387 [EXCERPT]
22388 a1
22389 ˇb1
22390 [EXCERPT]
22391 [FOLDED]
22392 [EXCERPT]
22393 [FOLDED]
22394 "
22395 });
22396 cx.simulate_keystroke("up");
22397 cx.assert_excerpts_with_selections(indoc! {"
22398 [EXCERPT]
22399 [FOLDED]
22400 [EXCERPT]
22401 ˇa1
22402 b1
22403 [EXCERPT]
22404 [FOLDED]
22405 [EXCERPT]
22406 [FOLDED]
22407 "
22408 });
22409 for _ in 0..5 {
22410 cx.simulate_keystroke("up");
22411 cx.assert_excerpts_with_selections(indoc! {"
22412 [EXCERPT]
22413 ˇ[FOLDED]
22414 [EXCERPT]
22415 a1
22416 b1
22417 [EXCERPT]
22418 [FOLDED]
22419 [EXCERPT]
22420 [FOLDED]
22421 "
22422 });
22423 }
22424}
22425
22426#[gpui::test]
22427async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22428 init_test(cx, |_| {});
22429
22430 // Simple insertion
22431 assert_highlighted_edits(
22432 "Hello, world!",
22433 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22434 true,
22435 cx,
22436 |highlighted_edits, cx| {
22437 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22438 assert_eq!(highlighted_edits.highlights.len(), 1);
22439 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22440 assert_eq!(
22441 highlighted_edits.highlights[0].1.background_color,
22442 Some(cx.theme().status().created_background)
22443 );
22444 },
22445 )
22446 .await;
22447
22448 // Replacement
22449 assert_highlighted_edits(
22450 "This is a test.",
22451 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22452 false,
22453 cx,
22454 |highlighted_edits, cx| {
22455 assert_eq!(highlighted_edits.text, "That is a test.");
22456 assert_eq!(highlighted_edits.highlights.len(), 1);
22457 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22458 assert_eq!(
22459 highlighted_edits.highlights[0].1.background_color,
22460 Some(cx.theme().status().created_background)
22461 );
22462 },
22463 )
22464 .await;
22465
22466 // Multiple edits
22467 assert_highlighted_edits(
22468 "Hello, world!",
22469 vec![
22470 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22471 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22472 ],
22473 false,
22474 cx,
22475 |highlighted_edits, cx| {
22476 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22477 assert_eq!(highlighted_edits.highlights.len(), 2);
22478 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22479 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22480 assert_eq!(
22481 highlighted_edits.highlights[0].1.background_color,
22482 Some(cx.theme().status().created_background)
22483 );
22484 assert_eq!(
22485 highlighted_edits.highlights[1].1.background_color,
22486 Some(cx.theme().status().created_background)
22487 );
22488 },
22489 )
22490 .await;
22491
22492 // Multiple lines with edits
22493 assert_highlighted_edits(
22494 "First line\nSecond line\nThird line\nFourth line",
22495 vec![
22496 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22497 (
22498 Point::new(2, 0)..Point::new(2, 10),
22499 "New third line".to_string(),
22500 ),
22501 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22502 ],
22503 false,
22504 cx,
22505 |highlighted_edits, cx| {
22506 assert_eq!(
22507 highlighted_edits.text,
22508 "Second modified\nNew third line\nFourth updated line"
22509 );
22510 assert_eq!(highlighted_edits.highlights.len(), 3);
22511 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22512 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22513 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22514 for highlight in &highlighted_edits.highlights {
22515 assert_eq!(
22516 highlight.1.background_color,
22517 Some(cx.theme().status().created_background)
22518 );
22519 }
22520 },
22521 )
22522 .await;
22523}
22524
22525#[gpui::test]
22526async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22527 init_test(cx, |_| {});
22528
22529 // Deletion
22530 assert_highlighted_edits(
22531 "Hello, world!",
22532 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22533 true,
22534 cx,
22535 |highlighted_edits, cx| {
22536 assert_eq!(highlighted_edits.text, "Hello, world!");
22537 assert_eq!(highlighted_edits.highlights.len(), 1);
22538 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22539 assert_eq!(
22540 highlighted_edits.highlights[0].1.background_color,
22541 Some(cx.theme().status().deleted_background)
22542 );
22543 },
22544 )
22545 .await;
22546
22547 // Insertion
22548 assert_highlighted_edits(
22549 "Hello, world!",
22550 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22551 true,
22552 cx,
22553 |highlighted_edits, cx| {
22554 assert_eq!(highlighted_edits.highlights.len(), 1);
22555 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22556 assert_eq!(
22557 highlighted_edits.highlights[0].1.background_color,
22558 Some(cx.theme().status().created_background)
22559 );
22560 },
22561 )
22562 .await;
22563}
22564
22565async fn assert_highlighted_edits(
22566 text: &str,
22567 edits: Vec<(Range<Point>, String)>,
22568 include_deletions: bool,
22569 cx: &mut TestAppContext,
22570 assertion_fn: impl Fn(HighlightedText, &App),
22571) {
22572 let window = cx.add_window(|window, cx| {
22573 let buffer = MultiBuffer::build_simple(text, cx);
22574 Editor::new(EditorMode::full(), buffer, None, window, cx)
22575 });
22576 let cx = &mut VisualTestContext::from_window(*window, cx);
22577
22578 let (buffer, snapshot) = window
22579 .update(cx, |editor, _window, cx| {
22580 (
22581 editor.buffer().clone(),
22582 editor.buffer().read(cx).snapshot(cx),
22583 )
22584 })
22585 .unwrap();
22586
22587 let edits = edits
22588 .into_iter()
22589 .map(|(range, edit)| {
22590 (
22591 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22592 edit,
22593 )
22594 })
22595 .collect::<Vec<_>>();
22596
22597 let text_anchor_edits = edits
22598 .clone()
22599 .into_iter()
22600 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22601 .collect::<Vec<_>>();
22602
22603 let edit_preview = window
22604 .update(cx, |_, _window, cx| {
22605 buffer
22606 .read(cx)
22607 .as_singleton()
22608 .unwrap()
22609 .read(cx)
22610 .preview_edits(text_anchor_edits.into(), cx)
22611 })
22612 .unwrap()
22613 .await;
22614
22615 cx.update(|_window, cx| {
22616 let highlighted_edits = edit_prediction_edit_text(
22617 snapshot.as_singleton().unwrap().2,
22618 &edits,
22619 &edit_preview,
22620 include_deletions,
22621 cx,
22622 );
22623 assertion_fn(highlighted_edits, cx)
22624 });
22625}
22626
22627#[track_caller]
22628fn assert_breakpoint(
22629 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22630 path: &Arc<Path>,
22631 expected: Vec<(u32, Breakpoint)>,
22632) {
22633 if expected.is_empty() {
22634 assert!(!breakpoints.contains_key(path), "{}", path.display());
22635 } else {
22636 let mut breakpoint = breakpoints
22637 .get(path)
22638 .unwrap()
22639 .iter()
22640 .map(|breakpoint| {
22641 (
22642 breakpoint.row,
22643 Breakpoint {
22644 message: breakpoint.message.clone(),
22645 state: breakpoint.state,
22646 condition: breakpoint.condition.clone(),
22647 hit_condition: breakpoint.hit_condition.clone(),
22648 },
22649 )
22650 })
22651 .collect::<Vec<_>>();
22652
22653 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22654
22655 assert_eq!(expected, breakpoint);
22656 }
22657}
22658
22659fn add_log_breakpoint_at_cursor(
22660 editor: &mut Editor,
22661 log_message: &str,
22662 window: &mut Window,
22663 cx: &mut Context<Editor>,
22664) {
22665 let (anchor, bp) = editor
22666 .breakpoints_at_cursors(window, cx)
22667 .first()
22668 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22669 .unwrap_or_else(|| {
22670 let cursor_position: Point = editor.selections.newest(cx).head();
22671
22672 let breakpoint_position = editor
22673 .snapshot(window, cx)
22674 .display_snapshot
22675 .buffer_snapshot()
22676 .anchor_before(Point::new(cursor_position.row, 0));
22677
22678 (breakpoint_position, Breakpoint::new_log(log_message))
22679 });
22680
22681 editor.edit_breakpoint_at_anchor(
22682 anchor,
22683 bp,
22684 BreakpointEditAction::EditLogMessage(log_message.into()),
22685 cx,
22686 );
22687}
22688
22689#[gpui::test]
22690async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22691 init_test(cx, |_| {});
22692
22693 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22694 let fs = FakeFs::new(cx.executor());
22695 fs.insert_tree(
22696 path!("/a"),
22697 json!({
22698 "main.rs": sample_text,
22699 }),
22700 )
22701 .await;
22702 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22703 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22704 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22705
22706 let fs = FakeFs::new(cx.executor());
22707 fs.insert_tree(
22708 path!("/a"),
22709 json!({
22710 "main.rs": sample_text,
22711 }),
22712 )
22713 .await;
22714 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22715 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22716 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22717 let worktree_id = workspace
22718 .update(cx, |workspace, _window, cx| {
22719 workspace.project().update(cx, |project, cx| {
22720 project.worktrees(cx).next().unwrap().read(cx).id()
22721 })
22722 })
22723 .unwrap();
22724
22725 let buffer = project
22726 .update(cx, |project, cx| {
22727 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22728 })
22729 .await
22730 .unwrap();
22731
22732 let (editor, cx) = cx.add_window_view(|window, cx| {
22733 Editor::new(
22734 EditorMode::full(),
22735 MultiBuffer::build_from_buffer(buffer, cx),
22736 Some(project.clone()),
22737 window,
22738 cx,
22739 )
22740 });
22741
22742 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22743 let abs_path = project.read_with(cx, |project, cx| {
22744 project
22745 .absolute_path(&project_path, cx)
22746 .map(Arc::from)
22747 .unwrap()
22748 });
22749
22750 // assert we can add breakpoint on the first line
22751 editor.update_in(cx, |editor, window, cx| {
22752 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22753 editor.move_to_end(&MoveToEnd, window, cx);
22754 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22755 });
22756
22757 let breakpoints = editor.update(cx, |editor, cx| {
22758 editor
22759 .breakpoint_store()
22760 .as_ref()
22761 .unwrap()
22762 .read(cx)
22763 .all_source_breakpoints(cx)
22764 });
22765
22766 assert_eq!(1, breakpoints.len());
22767 assert_breakpoint(
22768 &breakpoints,
22769 &abs_path,
22770 vec![
22771 (0, Breakpoint::new_standard()),
22772 (3, Breakpoint::new_standard()),
22773 ],
22774 );
22775
22776 editor.update_in(cx, |editor, window, cx| {
22777 editor.move_to_beginning(&MoveToBeginning, window, cx);
22778 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22779 });
22780
22781 let breakpoints = editor.update(cx, |editor, cx| {
22782 editor
22783 .breakpoint_store()
22784 .as_ref()
22785 .unwrap()
22786 .read(cx)
22787 .all_source_breakpoints(cx)
22788 });
22789
22790 assert_eq!(1, breakpoints.len());
22791 assert_breakpoint(
22792 &breakpoints,
22793 &abs_path,
22794 vec![(3, Breakpoint::new_standard())],
22795 );
22796
22797 editor.update_in(cx, |editor, window, cx| {
22798 editor.move_to_end(&MoveToEnd, window, cx);
22799 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22800 });
22801
22802 let breakpoints = editor.update(cx, |editor, cx| {
22803 editor
22804 .breakpoint_store()
22805 .as_ref()
22806 .unwrap()
22807 .read(cx)
22808 .all_source_breakpoints(cx)
22809 });
22810
22811 assert_eq!(0, breakpoints.len());
22812 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22813}
22814
22815#[gpui::test]
22816async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22817 init_test(cx, |_| {});
22818
22819 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22820
22821 let fs = FakeFs::new(cx.executor());
22822 fs.insert_tree(
22823 path!("/a"),
22824 json!({
22825 "main.rs": sample_text,
22826 }),
22827 )
22828 .await;
22829 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22830 let (workspace, cx) =
22831 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22832
22833 let worktree_id = workspace.update(cx, |workspace, cx| {
22834 workspace.project().update(cx, |project, cx| {
22835 project.worktrees(cx).next().unwrap().read(cx).id()
22836 })
22837 });
22838
22839 let buffer = project
22840 .update(cx, |project, cx| {
22841 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22842 })
22843 .await
22844 .unwrap();
22845
22846 let (editor, cx) = cx.add_window_view(|window, cx| {
22847 Editor::new(
22848 EditorMode::full(),
22849 MultiBuffer::build_from_buffer(buffer, cx),
22850 Some(project.clone()),
22851 window,
22852 cx,
22853 )
22854 });
22855
22856 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22857 let abs_path = project.read_with(cx, |project, cx| {
22858 project
22859 .absolute_path(&project_path, cx)
22860 .map(Arc::from)
22861 .unwrap()
22862 });
22863
22864 editor.update_in(cx, |editor, window, cx| {
22865 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22866 });
22867
22868 let breakpoints = editor.update(cx, |editor, cx| {
22869 editor
22870 .breakpoint_store()
22871 .as_ref()
22872 .unwrap()
22873 .read(cx)
22874 .all_source_breakpoints(cx)
22875 });
22876
22877 assert_breakpoint(
22878 &breakpoints,
22879 &abs_path,
22880 vec![(0, Breakpoint::new_log("hello world"))],
22881 );
22882
22883 // Removing a log message from a log breakpoint should remove it
22884 editor.update_in(cx, |editor, window, cx| {
22885 add_log_breakpoint_at_cursor(editor, "", window, cx);
22886 });
22887
22888 let breakpoints = editor.update(cx, |editor, cx| {
22889 editor
22890 .breakpoint_store()
22891 .as_ref()
22892 .unwrap()
22893 .read(cx)
22894 .all_source_breakpoints(cx)
22895 });
22896
22897 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22898
22899 editor.update_in(cx, |editor, window, cx| {
22900 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22901 editor.move_to_end(&MoveToEnd, window, cx);
22902 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22903 // Not adding a log message to a standard breakpoint shouldn't remove it
22904 add_log_breakpoint_at_cursor(editor, "", window, cx);
22905 });
22906
22907 let breakpoints = editor.update(cx, |editor, cx| {
22908 editor
22909 .breakpoint_store()
22910 .as_ref()
22911 .unwrap()
22912 .read(cx)
22913 .all_source_breakpoints(cx)
22914 });
22915
22916 assert_breakpoint(
22917 &breakpoints,
22918 &abs_path,
22919 vec![
22920 (0, Breakpoint::new_standard()),
22921 (3, Breakpoint::new_standard()),
22922 ],
22923 );
22924
22925 editor.update_in(cx, |editor, window, cx| {
22926 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22927 });
22928
22929 let breakpoints = editor.update(cx, |editor, cx| {
22930 editor
22931 .breakpoint_store()
22932 .as_ref()
22933 .unwrap()
22934 .read(cx)
22935 .all_source_breakpoints(cx)
22936 });
22937
22938 assert_breakpoint(
22939 &breakpoints,
22940 &abs_path,
22941 vec![
22942 (0, Breakpoint::new_standard()),
22943 (3, Breakpoint::new_log("hello world")),
22944 ],
22945 );
22946
22947 editor.update_in(cx, |editor, window, cx| {
22948 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22949 });
22950
22951 let breakpoints = editor.update(cx, |editor, cx| {
22952 editor
22953 .breakpoint_store()
22954 .as_ref()
22955 .unwrap()
22956 .read(cx)
22957 .all_source_breakpoints(cx)
22958 });
22959
22960 assert_breakpoint(
22961 &breakpoints,
22962 &abs_path,
22963 vec![
22964 (0, Breakpoint::new_standard()),
22965 (3, Breakpoint::new_log("hello Earth!!")),
22966 ],
22967 );
22968}
22969
22970/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22971/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22972/// or when breakpoints were placed out of order. This tests for a regression too
22973#[gpui::test]
22974async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22975 init_test(cx, |_| {});
22976
22977 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22978 let fs = FakeFs::new(cx.executor());
22979 fs.insert_tree(
22980 path!("/a"),
22981 json!({
22982 "main.rs": sample_text,
22983 }),
22984 )
22985 .await;
22986 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22987 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22988 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22989
22990 let fs = FakeFs::new(cx.executor());
22991 fs.insert_tree(
22992 path!("/a"),
22993 json!({
22994 "main.rs": sample_text,
22995 }),
22996 )
22997 .await;
22998 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22999 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23000 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23001 let worktree_id = workspace
23002 .update(cx, |workspace, _window, cx| {
23003 workspace.project().update(cx, |project, cx| {
23004 project.worktrees(cx).next().unwrap().read(cx).id()
23005 })
23006 })
23007 .unwrap();
23008
23009 let buffer = project
23010 .update(cx, |project, cx| {
23011 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23012 })
23013 .await
23014 .unwrap();
23015
23016 let (editor, cx) = cx.add_window_view(|window, cx| {
23017 Editor::new(
23018 EditorMode::full(),
23019 MultiBuffer::build_from_buffer(buffer, cx),
23020 Some(project.clone()),
23021 window,
23022 cx,
23023 )
23024 });
23025
23026 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23027 let abs_path = project.read_with(cx, |project, cx| {
23028 project
23029 .absolute_path(&project_path, cx)
23030 .map(Arc::from)
23031 .unwrap()
23032 });
23033
23034 // assert we can add breakpoint on the first line
23035 editor.update_in(cx, |editor, window, cx| {
23036 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23037 editor.move_to_end(&MoveToEnd, window, cx);
23038 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23039 editor.move_up(&MoveUp, window, cx);
23040 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23041 });
23042
23043 let breakpoints = editor.update(cx, |editor, cx| {
23044 editor
23045 .breakpoint_store()
23046 .as_ref()
23047 .unwrap()
23048 .read(cx)
23049 .all_source_breakpoints(cx)
23050 });
23051
23052 assert_eq!(1, breakpoints.len());
23053 assert_breakpoint(
23054 &breakpoints,
23055 &abs_path,
23056 vec![
23057 (0, Breakpoint::new_standard()),
23058 (2, Breakpoint::new_standard()),
23059 (3, Breakpoint::new_standard()),
23060 ],
23061 );
23062
23063 editor.update_in(cx, |editor, window, cx| {
23064 editor.move_to_beginning(&MoveToBeginning, window, cx);
23065 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23066 editor.move_to_end(&MoveToEnd, window, cx);
23067 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23068 // Disabling a breakpoint that doesn't exist should do nothing
23069 editor.move_up(&MoveUp, window, cx);
23070 editor.move_up(&MoveUp, window, cx);
23071 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23072 });
23073
23074 let breakpoints = editor.update(cx, |editor, cx| {
23075 editor
23076 .breakpoint_store()
23077 .as_ref()
23078 .unwrap()
23079 .read(cx)
23080 .all_source_breakpoints(cx)
23081 });
23082
23083 let disable_breakpoint = {
23084 let mut bp = Breakpoint::new_standard();
23085 bp.state = BreakpointState::Disabled;
23086 bp
23087 };
23088
23089 assert_eq!(1, breakpoints.len());
23090 assert_breakpoint(
23091 &breakpoints,
23092 &abs_path,
23093 vec![
23094 (0, disable_breakpoint.clone()),
23095 (2, Breakpoint::new_standard()),
23096 (3, disable_breakpoint.clone()),
23097 ],
23098 );
23099
23100 editor.update_in(cx, |editor, window, cx| {
23101 editor.move_to_beginning(&MoveToBeginning, window, cx);
23102 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23103 editor.move_to_end(&MoveToEnd, window, cx);
23104 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23105 editor.move_up(&MoveUp, window, cx);
23106 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23107 });
23108
23109 let breakpoints = editor.update(cx, |editor, cx| {
23110 editor
23111 .breakpoint_store()
23112 .as_ref()
23113 .unwrap()
23114 .read(cx)
23115 .all_source_breakpoints(cx)
23116 });
23117
23118 assert_eq!(1, breakpoints.len());
23119 assert_breakpoint(
23120 &breakpoints,
23121 &abs_path,
23122 vec![
23123 (0, Breakpoint::new_standard()),
23124 (2, disable_breakpoint),
23125 (3, Breakpoint::new_standard()),
23126 ],
23127 );
23128}
23129
23130#[gpui::test]
23131async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23132 init_test(cx, |_| {});
23133 let capabilities = lsp::ServerCapabilities {
23134 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23135 prepare_provider: Some(true),
23136 work_done_progress_options: Default::default(),
23137 })),
23138 ..Default::default()
23139 };
23140 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23141
23142 cx.set_state(indoc! {"
23143 struct Fˇoo {}
23144 "});
23145
23146 cx.update_editor(|editor, _, cx| {
23147 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23148 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23149 editor.highlight_background::<DocumentHighlightRead>(
23150 &[highlight_range],
23151 |theme| theme.colors().editor_document_highlight_read_background,
23152 cx,
23153 );
23154 });
23155
23156 let mut prepare_rename_handler = cx
23157 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23158 move |_, _, _| async move {
23159 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23160 start: lsp::Position {
23161 line: 0,
23162 character: 7,
23163 },
23164 end: lsp::Position {
23165 line: 0,
23166 character: 10,
23167 },
23168 })))
23169 },
23170 );
23171 let prepare_rename_task = cx
23172 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23173 .expect("Prepare rename was not started");
23174 prepare_rename_handler.next().await.unwrap();
23175 prepare_rename_task.await.expect("Prepare rename failed");
23176
23177 let mut rename_handler =
23178 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23179 let edit = lsp::TextEdit {
23180 range: lsp::Range {
23181 start: lsp::Position {
23182 line: 0,
23183 character: 7,
23184 },
23185 end: lsp::Position {
23186 line: 0,
23187 character: 10,
23188 },
23189 },
23190 new_text: "FooRenamed".to_string(),
23191 };
23192 Ok(Some(lsp::WorkspaceEdit::new(
23193 // Specify the same edit twice
23194 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23195 )))
23196 });
23197 let rename_task = cx
23198 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23199 .expect("Confirm rename was not started");
23200 rename_handler.next().await.unwrap();
23201 rename_task.await.expect("Confirm rename failed");
23202 cx.run_until_parked();
23203
23204 // Despite two edits, only one is actually applied as those are identical
23205 cx.assert_editor_state(indoc! {"
23206 struct FooRenamedˇ {}
23207 "});
23208}
23209
23210#[gpui::test]
23211async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23212 init_test(cx, |_| {});
23213 // These capabilities indicate that the server does not support prepare rename.
23214 let capabilities = lsp::ServerCapabilities {
23215 rename_provider: Some(lsp::OneOf::Left(true)),
23216 ..Default::default()
23217 };
23218 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23219
23220 cx.set_state(indoc! {"
23221 struct Fˇoo {}
23222 "});
23223
23224 cx.update_editor(|editor, _window, cx| {
23225 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23226 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23227 editor.highlight_background::<DocumentHighlightRead>(
23228 &[highlight_range],
23229 |theme| theme.colors().editor_document_highlight_read_background,
23230 cx,
23231 );
23232 });
23233
23234 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23235 .expect("Prepare rename was not started")
23236 .await
23237 .expect("Prepare rename failed");
23238
23239 let mut rename_handler =
23240 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23241 let edit = lsp::TextEdit {
23242 range: lsp::Range {
23243 start: lsp::Position {
23244 line: 0,
23245 character: 7,
23246 },
23247 end: lsp::Position {
23248 line: 0,
23249 character: 10,
23250 },
23251 },
23252 new_text: "FooRenamed".to_string(),
23253 };
23254 Ok(Some(lsp::WorkspaceEdit::new(
23255 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23256 )))
23257 });
23258 let rename_task = cx
23259 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23260 .expect("Confirm rename was not started");
23261 rename_handler.next().await.unwrap();
23262 rename_task.await.expect("Confirm rename failed");
23263 cx.run_until_parked();
23264
23265 // Correct range is renamed, as `surrounding_word` is used to find it.
23266 cx.assert_editor_state(indoc! {"
23267 struct FooRenamedˇ {}
23268 "});
23269}
23270
23271#[gpui::test]
23272async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23273 init_test(cx, |_| {});
23274 let mut cx = EditorTestContext::new(cx).await;
23275
23276 let language = Arc::new(
23277 Language::new(
23278 LanguageConfig::default(),
23279 Some(tree_sitter_html::LANGUAGE.into()),
23280 )
23281 .with_brackets_query(
23282 r#"
23283 ("<" @open "/>" @close)
23284 ("</" @open ">" @close)
23285 ("<" @open ">" @close)
23286 ("\"" @open "\"" @close)
23287 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23288 "#,
23289 )
23290 .unwrap(),
23291 );
23292 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23293
23294 cx.set_state(indoc! {"
23295 <span>ˇ</span>
23296 "});
23297 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23298 cx.assert_editor_state(indoc! {"
23299 <span>
23300 ˇ
23301 </span>
23302 "});
23303
23304 cx.set_state(indoc! {"
23305 <span><span></span>ˇ</span>
23306 "});
23307 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23308 cx.assert_editor_state(indoc! {"
23309 <span><span></span>
23310 ˇ</span>
23311 "});
23312
23313 cx.set_state(indoc! {"
23314 <span>ˇ
23315 </span>
23316 "});
23317 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23318 cx.assert_editor_state(indoc! {"
23319 <span>
23320 ˇ
23321 </span>
23322 "});
23323}
23324
23325#[gpui::test(iterations = 10)]
23326async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23327 init_test(cx, |_| {});
23328
23329 let fs = FakeFs::new(cx.executor());
23330 fs.insert_tree(
23331 path!("/dir"),
23332 json!({
23333 "a.ts": "a",
23334 }),
23335 )
23336 .await;
23337
23338 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23339 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23340 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23341
23342 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23343 language_registry.add(Arc::new(Language::new(
23344 LanguageConfig {
23345 name: "TypeScript".into(),
23346 matcher: LanguageMatcher {
23347 path_suffixes: vec!["ts".to_string()],
23348 ..Default::default()
23349 },
23350 ..Default::default()
23351 },
23352 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23353 )));
23354 let mut fake_language_servers = language_registry.register_fake_lsp(
23355 "TypeScript",
23356 FakeLspAdapter {
23357 capabilities: lsp::ServerCapabilities {
23358 code_lens_provider: Some(lsp::CodeLensOptions {
23359 resolve_provider: Some(true),
23360 }),
23361 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23362 commands: vec!["_the/command".to_string()],
23363 ..lsp::ExecuteCommandOptions::default()
23364 }),
23365 ..lsp::ServerCapabilities::default()
23366 },
23367 ..FakeLspAdapter::default()
23368 },
23369 );
23370
23371 let editor = workspace
23372 .update(cx, |workspace, window, cx| {
23373 workspace.open_abs_path(
23374 PathBuf::from(path!("/dir/a.ts")),
23375 OpenOptions::default(),
23376 window,
23377 cx,
23378 )
23379 })
23380 .unwrap()
23381 .await
23382 .unwrap()
23383 .downcast::<Editor>()
23384 .unwrap();
23385 cx.executor().run_until_parked();
23386
23387 let fake_server = fake_language_servers.next().await.unwrap();
23388
23389 let buffer = editor.update(cx, |editor, cx| {
23390 editor
23391 .buffer()
23392 .read(cx)
23393 .as_singleton()
23394 .expect("have opened a single file by path")
23395 });
23396
23397 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23398 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23399 drop(buffer_snapshot);
23400 let actions = cx
23401 .update_window(*workspace, |_, window, cx| {
23402 project.code_actions(&buffer, anchor..anchor, window, cx)
23403 })
23404 .unwrap();
23405
23406 fake_server
23407 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23408 Ok(Some(vec![
23409 lsp::CodeLens {
23410 range: lsp::Range::default(),
23411 command: Some(lsp::Command {
23412 title: "Code lens command".to_owned(),
23413 command: "_the/command".to_owned(),
23414 arguments: None,
23415 }),
23416 data: None,
23417 },
23418 lsp::CodeLens {
23419 range: lsp::Range::default(),
23420 command: Some(lsp::Command {
23421 title: "Command not in capabilities".to_owned(),
23422 command: "not in capabilities".to_owned(),
23423 arguments: None,
23424 }),
23425 data: None,
23426 },
23427 lsp::CodeLens {
23428 range: lsp::Range {
23429 start: lsp::Position {
23430 line: 1,
23431 character: 1,
23432 },
23433 end: lsp::Position {
23434 line: 1,
23435 character: 1,
23436 },
23437 },
23438 command: Some(lsp::Command {
23439 title: "Command not in range".to_owned(),
23440 command: "_the/command".to_owned(),
23441 arguments: None,
23442 }),
23443 data: None,
23444 },
23445 ]))
23446 })
23447 .next()
23448 .await;
23449
23450 let actions = actions.await.unwrap();
23451 assert_eq!(
23452 actions.len(),
23453 1,
23454 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23455 );
23456 let action = actions[0].clone();
23457 let apply = project.update(cx, |project, cx| {
23458 project.apply_code_action(buffer.clone(), action, true, cx)
23459 });
23460
23461 // Resolving the code action does not populate its edits. In absence of
23462 // edits, we must execute the given command.
23463 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23464 |mut lens, _| async move {
23465 let lens_command = lens.command.as_mut().expect("should have a command");
23466 assert_eq!(lens_command.title, "Code lens command");
23467 lens_command.arguments = Some(vec![json!("the-argument")]);
23468 Ok(lens)
23469 },
23470 );
23471
23472 // While executing the command, the language server sends the editor
23473 // a `workspaceEdit` request.
23474 fake_server
23475 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23476 let fake = fake_server.clone();
23477 move |params, _| {
23478 assert_eq!(params.command, "_the/command");
23479 let fake = fake.clone();
23480 async move {
23481 fake.server
23482 .request::<lsp::request::ApplyWorkspaceEdit>(
23483 lsp::ApplyWorkspaceEditParams {
23484 label: None,
23485 edit: lsp::WorkspaceEdit {
23486 changes: Some(
23487 [(
23488 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23489 vec![lsp::TextEdit {
23490 range: lsp::Range::new(
23491 lsp::Position::new(0, 0),
23492 lsp::Position::new(0, 0),
23493 ),
23494 new_text: "X".into(),
23495 }],
23496 )]
23497 .into_iter()
23498 .collect(),
23499 ),
23500 ..lsp::WorkspaceEdit::default()
23501 },
23502 },
23503 )
23504 .await
23505 .into_response()
23506 .unwrap();
23507 Ok(Some(json!(null)))
23508 }
23509 }
23510 })
23511 .next()
23512 .await;
23513
23514 // Applying the code lens command returns a project transaction containing the edits
23515 // sent by the language server in its `workspaceEdit` request.
23516 let transaction = apply.await.unwrap();
23517 assert!(transaction.0.contains_key(&buffer));
23518 buffer.update(cx, |buffer, cx| {
23519 assert_eq!(buffer.text(), "Xa");
23520 buffer.undo(cx);
23521 assert_eq!(buffer.text(), "a");
23522 });
23523
23524 let actions_after_edits = cx
23525 .update_window(*workspace, |_, window, cx| {
23526 project.code_actions(&buffer, anchor..anchor, window, cx)
23527 })
23528 .unwrap()
23529 .await
23530 .unwrap();
23531 assert_eq!(
23532 actions, actions_after_edits,
23533 "For the same selection, same code lens actions should be returned"
23534 );
23535
23536 let _responses =
23537 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23538 panic!("No more code lens requests are expected");
23539 });
23540 editor.update_in(cx, |editor, window, cx| {
23541 editor.select_all(&SelectAll, window, cx);
23542 });
23543 cx.executor().run_until_parked();
23544 let new_actions = cx
23545 .update_window(*workspace, |_, window, cx| {
23546 project.code_actions(&buffer, anchor..anchor, window, cx)
23547 })
23548 .unwrap()
23549 .await
23550 .unwrap();
23551 assert_eq!(
23552 actions, new_actions,
23553 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23554 );
23555}
23556
23557#[gpui::test]
23558async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23559 init_test(cx, |_| {});
23560
23561 let fs = FakeFs::new(cx.executor());
23562 let main_text = r#"fn main() {
23563println!("1");
23564println!("2");
23565println!("3");
23566println!("4");
23567println!("5");
23568}"#;
23569 let lib_text = "mod foo {}";
23570 fs.insert_tree(
23571 path!("/a"),
23572 json!({
23573 "lib.rs": lib_text,
23574 "main.rs": main_text,
23575 }),
23576 )
23577 .await;
23578
23579 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23580 let (workspace, cx) =
23581 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23582 let worktree_id = workspace.update(cx, |workspace, cx| {
23583 workspace.project().update(cx, |project, cx| {
23584 project.worktrees(cx).next().unwrap().read(cx).id()
23585 })
23586 });
23587
23588 let expected_ranges = vec![
23589 Point::new(0, 0)..Point::new(0, 0),
23590 Point::new(1, 0)..Point::new(1, 1),
23591 Point::new(2, 0)..Point::new(2, 2),
23592 Point::new(3, 0)..Point::new(3, 3),
23593 ];
23594
23595 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23596 let editor_1 = workspace
23597 .update_in(cx, |workspace, window, cx| {
23598 workspace.open_path(
23599 (worktree_id, rel_path("main.rs")),
23600 Some(pane_1.downgrade()),
23601 true,
23602 window,
23603 cx,
23604 )
23605 })
23606 .unwrap()
23607 .await
23608 .downcast::<Editor>()
23609 .unwrap();
23610 pane_1.update(cx, |pane, cx| {
23611 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23612 open_editor.update(cx, |editor, cx| {
23613 assert_eq!(
23614 editor.display_text(cx),
23615 main_text,
23616 "Original main.rs text on initial open",
23617 );
23618 assert_eq!(
23619 editor
23620 .selections
23621 .all::<Point>(cx)
23622 .into_iter()
23623 .map(|s| s.range())
23624 .collect::<Vec<_>>(),
23625 vec![Point::zero()..Point::zero()],
23626 "Default selections on initial open",
23627 );
23628 })
23629 });
23630 editor_1.update_in(cx, |editor, window, cx| {
23631 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23632 s.select_ranges(expected_ranges.clone());
23633 });
23634 });
23635
23636 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23637 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23638 });
23639 let editor_2 = workspace
23640 .update_in(cx, |workspace, window, cx| {
23641 workspace.open_path(
23642 (worktree_id, rel_path("main.rs")),
23643 Some(pane_2.downgrade()),
23644 true,
23645 window,
23646 cx,
23647 )
23648 })
23649 .unwrap()
23650 .await
23651 .downcast::<Editor>()
23652 .unwrap();
23653 pane_2.update(cx, |pane, cx| {
23654 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23655 open_editor.update(cx, |editor, cx| {
23656 assert_eq!(
23657 editor.display_text(cx),
23658 main_text,
23659 "Original main.rs text on initial open in another panel",
23660 );
23661 assert_eq!(
23662 editor
23663 .selections
23664 .all::<Point>(cx)
23665 .into_iter()
23666 .map(|s| s.range())
23667 .collect::<Vec<_>>(),
23668 vec![Point::zero()..Point::zero()],
23669 "Default selections on initial open in another panel",
23670 );
23671 })
23672 });
23673
23674 editor_2.update_in(cx, |editor, window, cx| {
23675 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23676 });
23677
23678 let _other_editor_1 = workspace
23679 .update_in(cx, |workspace, window, cx| {
23680 workspace.open_path(
23681 (worktree_id, rel_path("lib.rs")),
23682 Some(pane_1.downgrade()),
23683 true,
23684 window,
23685 cx,
23686 )
23687 })
23688 .unwrap()
23689 .await
23690 .downcast::<Editor>()
23691 .unwrap();
23692 pane_1
23693 .update_in(cx, |pane, window, cx| {
23694 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23695 })
23696 .await
23697 .unwrap();
23698 drop(editor_1);
23699 pane_1.update(cx, |pane, cx| {
23700 pane.active_item()
23701 .unwrap()
23702 .downcast::<Editor>()
23703 .unwrap()
23704 .update(cx, |editor, cx| {
23705 assert_eq!(
23706 editor.display_text(cx),
23707 lib_text,
23708 "Other file should be open and active",
23709 );
23710 });
23711 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23712 });
23713
23714 let _other_editor_2 = workspace
23715 .update_in(cx, |workspace, window, cx| {
23716 workspace.open_path(
23717 (worktree_id, rel_path("lib.rs")),
23718 Some(pane_2.downgrade()),
23719 true,
23720 window,
23721 cx,
23722 )
23723 })
23724 .unwrap()
23725 .await
23726 .downcast::<Editor>()
23727 .unwrap();
23728 pane_2
23729 .update_in(cx, |pane, window, cx| {
23730 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23731 })
23732 .await
23733 .unwrap();
23734 drop(editor_2);
23735 pane_2.update(cx, |pane, cx| {
23736 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23737 open_editor.update(cx, |editor, cx| {
23738 assert_eq!(
23739 editor.display_text(cx),
23740 lib_text,
23741 "Other file should be open and active in another panel too",
23742 );
23743 });
23744 assert_eq!(
23745 pane.items().count(),
23746 1,
23747 "No other editors should be open in another pane",
23748 );
23749 });
23750
23751 let _editor_1_reopened = workspace
23752 .update_in(cx, |workspace, window, cx| {
23753 workspace.open_path(
23754 (worktree_id, rel_path("main.rs")),
23755 Some(pane_1.downgrade()),
23756 true,
23757 window,
23758 cx,
23759 )
23760 })
23761 .unwrap()
23762 .await
23763 .downcast::<Editor>()
23764 .unwrap();
23765 let _editor_2_reopened = workspace
23766 .update_in(cx, |workspace, window, cx| {
23767 workspace.open_path(
23768 (worktree_id, rel_path("main.rs")),
23769 Some(pane_2.downgrade()),
23770 true,
23771 window,
23772 cx,
23773 )
23774 })
23775 .unwrap()
23776 .await
23777 .downcast::<Editor>()
23778 .unwrap();
23779 pane_1.update(cx, |pane, cx| {
23780 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23781 open_editor.update(cx, |editor, cx| {
23782 assert_eq!(
23783 editor.display_text(cx),
23784 main_text,
23785 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23786 );
23787 assert_eq!(
23788 editor
23789 .selections
23790 .all::<Point>(cx)
23791 .into_iter()
23792 .map(|s| s.range())
23793 .collect::<Vec<_>>(),
23794 expected_ranges,
23795 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23796 );
23797 })
23798 });
23799 pane_2.update(cx, |pane, cx| {
23800 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23801 open_editor.update(cx, |editor, cx| {
23802 assert_eq!(
23803 editor.display_text(cx),
23804 r#"fn main() {
23805⋯rintln!("1");
23806⋯intln!("2");
23807⋯ntln!("3");
23808println!("4");
23809println!("5");
23810}"#,
23811 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23812 );
23813 assert_eq!(
23814 editor
23815 .selections
23816 .all::<Point>(cx)
23817 .into_iter()
23818 .map(|s| s.range())
23819 .collect::<Vec<_>>(),
23820 vec![Point::zero()..Point::zero()],
23821 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23822 );
23823 })
23824 });
23825}
23826
23827#[gpui::test]
23828async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23829 init_test(cx, |_| {});
23830
23831 let fs = FakeFs::new(cx.executor());
23832 let main_text = r#"fn main() {
23833println!("1");
23834println!("2");
23835println!("3");
23836println!("4");
23837println!("5");
23838}"#;
23839 let lib_text = "mod foo {}";
23840 fs.insert_tree(
23841 path!("/a"),
23842 json!({
23843 "lib.rs": lib_text,
23844 "main.rs": main_text,
23845 }),
23846 )
23847 .await;
23848
23849 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23850 let (workspace, cx) =
23851 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23852 let worktree_id = workspace.update(cx, |workspace, cx| {
23853 workspace.project().update(cx, |project, cx| {
23854 project.worktrees(cx).next().unwrap().read(cx).id()
23855 })
23856 });
23857
23858 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23859 let editor = workspace
23860 .update_in(cx, |workspace, window, cx| {
23861 workspace.open_path(
23862 (worktree_id, rel_path("main.rs")),
23863 Some(pane.downgrade()),
23864 true,
23865 window,
23866 cx,
23867 )
23868 })
23869 .unwrap()
23870 .await
23871 .downcast::<Editor>()
23872 .unwrap();
23873 pane.update(cx, |pane, cx| {
23874 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23875 open_editor.update(cx, |editor, cx| {
23876 assert_eq!(
23877 editor.display_text(cx),
23878 main_text,
23879 "Original main.rs text on initial open",
23880 );
23881 })
23882 });
23883 editor.update_in(cx, |editor, window, cx| {
23884 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23885 });
23886
23887 cx.update_global(|store: &mut SettingsStore, cx| {
23888 store.update_user_settings(cx, |s| {
23889 s.workspace.restore_on_file_reopen = Some(false);
23890 });
23891 });
23892 editor.update_in(cx, |editor, window, cx| {
23893 editor.fold_ranges(
23894 vec![
23895 Point::new(1, 0)..Point::new(1, 1),
23896 Point::new(2, 0)..Point::new(2, 2),
23897 Point::new(3, 0)..Point::new(3, 3),
23898 ],
23899 false,
23900 window,
23901 cx,
23902 );
23903 });
23904 pane.update_in(cx, |pane, window, cx| {
23905 pane.close_all_items(&CloseAllItems::default(), window, cx)
23906 })
23907 .await
23908 .unwrap();
23909 pane.update(cx, |pane, _| {
23910 assert!(pane.active_item().is_none());
23911 });
23912 cx.update_global(|store: &mut SettingsStore, cx| {
23913 store.update_user_settings(cx, |s| {
23914 s.workspace.restore_on_file_reopen = Some(true);
23915 });
23916 });
23917
23918 let _editor_reopened = workspace
23919 .update_in(cx, |workspace, window, cx| {
23920 workspace.open_path(
23921 (worktree_id, rel_path("main.rs")),
23922 Some(pane.downgrade()),
23923 true,
23924 window,
23925 cx,
23926 )
23927 })
23928 .unwrap()
23929 .await
23930 .downcast::<Editor>()
23931 .unwrap();
23932 pane.update(cx, |pane, cx| {
23933 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23934 open_editor.update(cx, |editor, cx| {
23935 assert_eq!(
23936 editor.display_text(cx),
23937 main_text,
23938 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23939 );
23940 })
23941 });
23942}
23943
23944#[gpui::test]
23945async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23946 struct EmptyModalView {
23947 focus_handle: gpui::FocusHandle,
23948 }
23949 impl EventEmitter<DismissEvent> for EmptyModalView {}
23950 impl Render for EmptyModalView {
23951 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23952 div()
23953 }
23954 }
23955 impl Focusable for EmptyModalView {
23956 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23957 self.focus_handle.clone()
23958 }
23959 }
23960 impl workspace::ModalView for EmptyModalView {}
23961 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23962 EmptyModalView {
23963 focus_handle: cx.focus_handle(),
23964 }
23965 }
23966
23967 init_test(cx, |_| {});
23968
23969 let fs = FakeFs::new(cx.executor());
23970 let project = Project::test(fs, [], cx).await;
23971 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23972 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23973 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23974 let editor = cx.new_window_entity(|window, cx| {
23975 Editor::new(
23976 EditorMode::full(),
23977 buffer,
23978 Some(project.clone()),
23979 window,
23980 cx,
23981 )
23982 });
23983 workspace
23984 .update(cx, |workspace, window, cx| {
23985 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23986 })
23987 .unwrap();
23988 editor.update_in(cx, |editor, window, cx| {
23989 editor.open_context_menu(&OpenContextMenu, window, cx);
23990 assert!(editor.mouse_context_menu.is_some());
23991 });
23992 workspace
23993 .update(cx, |workspace, window, cx| {
23994 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23995 })
23996 .unwrap();
23997 cx.read(|cx| {
23998 assert!(editor.read(cx).mouse_context_menu.is_none());
23999 });
24000}
24001
24002fn set_linked_edit_ranges(
24003 opening: (Point, Point),
24004 closing: (Point, Point),
24005 editor: &mut Editor,
24006 cx: &mut Context<Editor>,
24007) {
24008 let Some((buffer, _)) = editor
24009 .buffer
24010 .read(cx)
24011 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24012 else {
24013 panic!("Failed to get buffer for selection position");
24014 };
24015 let buffer = buffer.read(cx);
24016 let buffer_id = buffer.remote_id();
24017 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24018 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24019 let mut linked_ranges = HashMap::default();
24020 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24021 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24022}
24023
24024#[gpui::test]
24025async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24026 init_test(cx, |_| {});
24027
24028 let fs = FakeFs::new(cx.executor());
24029 fs.insert_file(path!("/file.html"), Default::default())
24030 .await;
24031
24032 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24033
24034 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24035 let html_language = Arc::new(Language::new(
24036 LanguageConfig {
24037 name: "HTML".into(),
24038 matcher: LanguageMatcher {
24039 path_suffixes: vec!["html".to_string()],
24040 ..LanguageMatcher::default()
24041 },
24042 brackets: BracketPairConfig {
24043 pairs: vec![BracketPair {
24044 start: "<".into(),
24045 end: ">".into(),
24046 close: true,
24047 ..Default::default()
24048 }],
24049 ..Default::default()
24050 },
24051 ..Default::default()
24052 },
24053 Some(tree_sitter_html::LANGUAGE.into()),
24054 ));
24055 language_registry.add(html_language);
24056 let mut fake_servers = language_registry.register_fake_lsp(
24057 "HTML",
24058 FakeLspAdapter {
24059 capabilities: lsp::ServerCapabilities {
24060 completion_provider: Some(lsp::CompletionOptions {
24061 resolve_provider: Some(true),
24062 ..Default::default()
24063 }),
24064 ..Default::default()
24065 },
24066 ..Default::default()
24067 },
24068 );
24069
24070 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24071 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24072
24073 let worktree_id = workspace
24074 .update(cx, |workspace, _window, cx| {
24075 workspace.project().update(cx, |project, cx| {
24076 project.worktrees(cx).next().unwrap().read(cx).id()
24077 })
24078 })
24079 .unwrap();
24080 project
24081 .update(cx, |project, cx| {
24082 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24083 })
24084 .await
24085 .unwrap();
24086 let editor = workspace
24087 .update(cx, |workspace, window, cx| {
24088 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24089 })
24090 .unwrap()
24091 .await
24092 .unwrap()
24093 .downcast::<Editor>()
24094 .unwrap();
24095
24096 let fake_server = fake_servers.next().await.unwrap();
24097 editor.update_in(cx, |editor, window, cx| {
24098 editor.set_text("<ad></ad>", window, cx);
24099 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24100 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24101 });
24102 set_linked_edit_ranges(
24103 (Point::new(0, 1), Point::new(0, 3)),
24104 (Point::new(0, 6), Point::new(0, 8)),
24105 editor,
24106 cx,
24107 );
24108 });
24109 let mut completion_handle =
24110 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24111 Ok(Some(lsp::CompletionResponse::Array(vec![
24112 lsp::CompletionItem {
24113 label: "head".to_string(),
24114 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24115 lsp::InsertReplaceEdit {
24116 new_text: "head".to_string(),
24117 insert: lsp::Range::new(
24118 lsp::Position::new(0, 1),
24119 lsp::Position::new(0, 3),
24120 ),
24121 replace: lsp::Range::new(
24122 lsp::Position::new(0, 1),
24123 lsp::Position::new(0, 3),
24124 ),
24125 },
24126 )),
24127 ..Default::default()
24128 },
24129 ])))
24130 });
24131 editor.update_in(cx, |editor, window, cx| {
24132 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
24133 });
24134 cx.run_until_parked();
24135 completion_handle.next().await.unwrap();
24136 editor.update(cx, |editor, _| {
24137 assert!(
24138 editor.context_menu_visible(),
24139 "Completion menu should be visible"
24140 );
24141 });
24142 editor.update_in(cx, |editor, window, cx| {
24143 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24144 });
24145 cx.executor().run_until_parked();
24146 editor.update(cx, |editor, cx| {
24147 assert_eq!(editor.text(cx), "<head></head>");
24148 });
24149}
24150
24151#[gpui::test]
24152async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24153 init_test(cx, |_| {});
24154
24155 let mut cx = EditorTestContext::new(cx).await;
24156 let language = Arc::new(Language::new(
24157 LanguageConfig {
24158 name: "TSX".into(),
24159 matcher: LanguageMatcher {
24160 path_suffixes: vec!["tsx".to_string()],
24161 ..LanguageMatcher::default()
24162 },
24163 brackets: BracketPairConfig {
24164 pairs: vec![BracketPair {
24165 start: "<".into(),
24166 end: ">".into(),
24167 close: true,
24168 ..Default::default()
24169 }],
24170 ..Default::default()
24171 },
24172 linked_edit_characters: HashSet::from_iter(['.']),
24173 ..Default::default()
24174 },
24175 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24176 ));
24177 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24178
24179 // Test typing > does not extend linked pair
24180 cx.set_state("<divˇ<div></div>");
24181 cx.update_editor(|editor, _, cx| {
24182 set_linked_edit_ranges(
24183 (Point::new(0, 1), Point::new(0, 4)),
24184 (Point::new(0, 11), Point::new(0, 14)),
24185 editor,
24186 cx,
24187 );
24188 });
24189 cx.update_editor(|editor, window, cx| {
24190 editor.handle_input(">", window, cx);
24191 });
24192 cx.assert_editor_state("<div>ˇ<div></div>");
24193
24194 // Test typing . do extend linked pair
24195 cx.set_state("<Animatedˇ></Animated>");
24196 cx.update_editor(|editor, _, cx| {
24197 set_linked_edit_ranges(
24198 (Point::new(0, 1), Point::new(0, 9)),
24199 (Point::new(0, 12), Point::new(0, 20)),
24200 editor,
24201 cx,
24202 );
24203 });
24204 cx.update_editor(|editor, window, cx| {
24205 editor.handle_input(".", window, cx);
24206 });
24207 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24208 cx.update_editor(|editor, _, cx| {
24209 set_linked_edit_ranges(
24210 (Point::new(0, 1), Point::new(0, 10)),
24211 (Point::new(0, 13), Point::new(0, 21)),
24212 editor,
24213 cx,
24214 );
24215 });
24216 cx.update_editor(|editor, window, cx| {
24217 editor.handle_input("V", window, cx);
24218 });
24219 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24220}
24221
24222#[gpui::test]
24223async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24224 init_test(cx, |_| {});
24225
24226 let fs = FakeFs::new(cx.executor());
24227 fs.insert_tree(
24228 path!("/root"),
24229 json!({
24230 "a": {
24231 "main.rs": "fn main() {}",
24232 },
24233 "foo": {
24234 "bar": {
24235 "external_file.rs": "pub mod external {}",
24236 }
24237 }
24238 }),
24239 )
24240 .await;
24241
24242 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24243 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24244 language_registry.add(rust_lang());
24245 let _fake_servers = language_registry.register_fake_lsp(
24246 "Rust",
24247 FakeLspAdapter {
24248 ..FakeLspAdapter::default()
24249 },
24250 );
24251 let (workspace, cx) =
24252 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24253 let worktree_id = workspace.update(cx, |workspace, cx| {
24254 workspace.project().update(cx, |project, cx| {
24255 project.worktrees(cx).next().unwrap().read(cx).id()
24256 })
24257 });
24258
24259 let assert_language_servers_count =
24260 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24261 project.update(cx, |project, cx| {
24262 let current = project
24263 .lsp_store()
24264 .read(cx)
24265 .as_local()
24266 .unwrap()
24267 .language_servers
24268 .len();
24269 assert_eq!(expected, current, "{context}");
24270 });
24271 };
24272
24273 assert_language_servers_count(
24274 0,
24275 "No servers should be running before any file is open",
24276 cx,
24277 );
24278 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24279 let main_editor = workspace
24280 .update_in(cx, |workspace, window, cx| {
24281 workspace.open_path(
24282 (worktree_id, rel_path("main.rs")),
24283 Some(pane.downgrade()),
24284 true,
24285 window,
24286 cx,
24287 )
24288 })
24289 .unwrap()
24290 .await
24291 .downcast::<Editor>()
24292 .unwrap();
24293 pane.update(cx, |pane, cx| {
24294 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24295 open_editor.update(cx, |editor, cx| {
24296 assert_eq!(
24297 editor.display_text(cx),
24298 "fn main() {}",
24299 "Original main.rs text on initial open",
24300 );
24301 });
24302 assert_eq!(open_editor, main_editor);
24303 });
24304 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24305
24306 let external_editor = workspace
24307 .update_in(cx, |workspace, window, cx| {
24308 workspace.open_abs_path(
24309 PathBuf::from("/root/foo/bar/external_file.rs"),
24310 OpenOptions::default(),
24311 window,
24312 cx,
24313 )
24314 })
24315 .await
24316 .expect("opening external file")
24317 .downcast::<Editor>()
24318 .expect("downcasted external file's open element to editor");
24319 pane.update(cx, |pane, cx| {
24320 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24321 open_editor.update(cx, |editor, cx| {
24322 assert_eq!(
24323 editor.display_text(cx),
24324 "pub mod external {}",
24325 "External file is open now",
24326 );
24327 });
24328 assert_eq!(open_editor, external_editor);
24329 });
24330 assert_language_servers_count(
24331 1,
24332 "Second, external, *.rs file should join the existing server",
24333 cx,
24334 );
24335
24336 pane.update_in(cx, |pane, window, cx| {
24337 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24338 })
24339 .await
24340 .unwrap();
24341 pane.update_in(cx, |pane, window, cx| {
24342 pane.navigate_backward(&Default::default(), window, cx);
24343 });
24344 cx.run_until_parked();
24345 pane.update(cx, |pane, cx| {
24346 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24347 open_editor.update(cx, |editor, cx| {
24348 assert_eq!(
24349 editor.display_text(cx),
24350 "pub mod external {}",
24351 "External file is open now",
24352 );
24353 });
24354 });
24355 assert_language_servers_count(
24356 1,
24357 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24358 cx,
24359 );
24360
24361 cx.update(|_, cx| {
24362 workspace::reload(cx);
24363 });
24364 assert_language_servers_count(
24365 1,
24366 "After reloading the worktree with local and external files opened, only one project should be started",
24367 cx,
24368 );
24369}
24370
24371#[gpui::test]
24372async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24373 init_test(cx, |_| {});
24374
24375 let mut cx = EditorTestContext::new(cx).await;
24376 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24377 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24378
24379 // test cursor move to start of each line on tab
24380 // for `if`, `elif`, `else`, `while`, `with` and `for`
24381 cx.set_state(indoc! {"
24382 def main():
24383 ˇ for item in items:
24384 ˇ while item.active:
24385 ˇ if item.value > 10:
24386 ˇ continue
24387 ˇ elif item.value < 0:
24388 ˇ break
24389 ˇ else:
24390 ˇ with item.context() as ctx:
24391 ˇ yield count
24392 ˇ else:
24393 ˇ log('while else')
24394 ˇ else:
24395 ˇ log('for else')
24396 "});
24397 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24398 cx.assert_editor_state(indoc! {"
24399 def main():
24400 ˇfor item in items:
24401 ˇwhile item.active:
24402 ˇif item.value > 10:
24403 ˇcontinue
24404 ˇelif item.value < 0:
24405 ˇbreak
24406 ˇelse:
24407 ˇwith item.context() as ctx:
24408 ˇyield count
24409 ˇelse:
24410 ˇlog('while else')
24411 ˇelse:
24412 ˇlog('for else')
24413 "});
24414 // test relative indent is preserved when tab
24415 // for `if`, `elif`, `else`, `while`, `with` and `for`
24416 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24417 cx.assert_editor_state(indoc! {"
24418 def main():
24419 ˇfor item in items:
24420 ˇwhile item.active:
24421 ˇif item.value > 10:
24422 ˇcontinue
24423 ˇelif item.value < 0:
24424 ˇbreak
24425 ˇelse:
24426 ˇwith item.context() as ctx:
24427 ˇyield count
24428 ˇelse:
24429 ˇlog('while else')
24430 ˇelse:
24431 ˇlog('for else')
24432 "});
24433
24434 // test cursor move to start of each line on tab
24435 // for `try`, `except`, `else`, `finally`, `match` and `def`
24436 cx.set_state(indoc! {"
24437 def main():
24438 ˇ try:
24439 ˇ fetch()
24440 ˇ except ValueError:
24441 ˇ handle_error()
24442 ˇ else:
24443 ˇ match value:
24444 ˇ case _:
24445 ˇ finally:
24446 ˇ def status():
24447 ˇ return 0
24448 "});
24449 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24450 cx.assert_editor_state(indoc! {"
24451 def main():
24452 ˇtry:
24453 ˇfetch()
24454 ˇexcept ValueError:
24455 ˇhandle_error()
24456 ˇelse:
24457 ˇmatch value:
24458 ˇcase _:
24459 ˇfinally:
24460 ˇdef status():
24461 ˇreturn 0
24462 "});
24463 // test relative indent is preserved when tab
24464 // for `try`, `except`, `else`, `finally`, `match` and `def`
24465 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24466 cx.assert_editor_state(indoc! {"
24467 def main():
24468 ˇtry:
24469 ˇfetch()
24470 ˇexcept ValueError:
24471 ˇhandle_error()
24472 ˇelse:
24473 ˇmatch value:
24474 ˇcase _:
24475 ˇfinally:
24476 ˇdef status():
24477 ˇreturn 0
24478 "});
24479}
24480
24481#[gpui::test]
24482async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24483 init_test(cx, |_| {});
24484
24485 let mut cx = EditorTestContext::new(cx).await;
24486 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24487 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24488
24489 // test `else` auto outdents when typed inside `if` block
24490 cx.set_state(indoc! {"
24491 def main():
24492 if i == 2:
24493 return
24494 ˇ
24495 "});
24496 cx.update_editor(|editor, window, cx| {
24497 editor.handle_input("else:", window, cx);
24498 });
24499 cx.assert_editor_state(indoc! {"
24500 def main():
24501 if i == 2:
24502 return
24503 else:ˇ
24504 "});
24505
24506 // test `except` auto outdents when typed inside `try` block
24507 cx.set_state(indoc! {"
24508 def main():
24509 try:
24510 i = 2
24511 ˇ
24512 "});
24513 cx.update_editor(|editor, window, cx| {
24514 editor.handle_input("except:", window, cx);
24515 });
24516 cx.assert_editor_state(indoc! {"
24517 def main():
24518 try:
24519 i = 2
24520 except:ˇ
24521 "});
24522
24523 // test `else` auto outdents when typed inside `except` block
24524 cx.set_state(indoc! {"
24525 def main():
24526 try:
24527 i = 2
24528 except:
24529 j = 2
24530 ˇ
24531 "});
24532 cx.update_editor(|editor, window, cx| {
24533 editor.handle_input("else:", window, cx);
24534 });
24535 cx.assert_editor_state(indoc! {"
24536 def main():
24537 try:
24538 i = 2
24539 except:
24540 j = 2
24541 else:ˇ
24542 "});
24543
24544 // test `finally` auto outdents when typed inside `else` block
24545 cx.set_state(indoc! {"
24546 def main():
24547 try:
24548 i = 2
24549 except:
24550 j = 2
24551 else:
24552 k = 2
24553 ˇ
24554 "});
24555 cx.update_editor(|editor, window, cx| {
24556 editor.handle_input("finally:", window, cx);
24557 });
24558 cx.assert_editor_state(indoc! {"
24559 def main():
24560 try:
24561 i = 2
24562 except:
24563 j = 2
24564 else:
24565 k = 2
24566 finally:ˇ
24567 "});
24568
24569 // test `else` does not outdents when typed inside `except` block right after for block
24570 cx.set_state(indoc! {"
24571 def main():
24572 try:
24573 i = 2
24574 except:
24575 for i in range(n):
24576 pass
24577 ˇ
24578 "});
24579 cx.update_editor(|editor, window, cx| {
24580 editor.handle_input("else:", window, cx);
24581 });
24582 cx.assert_editor_state(indoc! {"
24583 def main():
24584 try:
24585 i = 2
24586 except:
24587 for i in range(n):
24588 pass
24589 else:ˇ
24590 "});
24591
24592 // test `finally` auto outdents when typed inside `else` block right after for block
24593 cx.set_state(indoc! {"
24594 def main():
24595 try:
24596 i = 2
24597 except:
24598 j = 2
24599 else:
24600 for i in range(n):
24601 pass
24602 ˇ
24603 "});
24604 cx.update_editor(|editor, window, cx| {
24605 editor.handle_input("finally:", window, cx);
24606 });
24607 cx.assert_editor_state(indoc! {"
24608 def main():
24609 try:
24610 i = 2
24611 except:
24612 j = 2
24613 else:
24614 for i in range(n):
24615 pass
24616 finally:ˇ
24617 "});
24618
24619 // test `except` outdents to inner "try" block
24620 cx.set_state(indoc! {"
24621 def main():
24622 try:
24623 i = 2
24624 if i == 2:
24625 try:
24626 i = 3
24627 ˇ
24628 "});
24629 cx.update_editor(|editor, window, cx| {
24630 editor.handle_input("except:", window, cx);
24631 });
24632 cx.assert_editor_state(indoc! {"
24633 def main():
24634 try:
24635 i = 2
24636 if i == 2:
24637 try:
24638 i = 3
24639 except:ˇ
24640 "});
24641
24642 // test `except` outdents to outer "try" block
24643 cx.set_state(indoc! {"
24644 def main():
24645 try:
24646 i = 2
24647 if i == 2:
24648 try:
24649 i = 3
24650 ˇ
24651 "});
24652 cx.update_editor(|editor, window, cx| {
24653 editor.handle_input("except:", window, cx);
24654 });
24655 cx.assert_editor_state(indoc! {"
24656 def main():
24657 try:
24658 i = 2
24659 if i == 2:
24660 try:
24661 i = 3
24662 except:ˇ
24663 "});
24664
24665 // test `else` stays at correct indent when typed after `for` block
24666 cx.set_state(indoc! {"
24667 def main():
24668 for i in range(10):
24669 if i == 3:
24670 break
24671 ˇ
24672 "});
24673 cx.update_editor(|editor, window, cx| {
24674 editor.handle_input("else:", window, cx);
24675 });
24676 cx.assert_editor_state(indoc! {"
24677 def main():
24678 for i in range(10):
24679 if i == 3:
24680 break
24681 else:ˇ
24682 "});
24683
24684 // test does not outdent on typing after line with square brackets
24685 cx.set_state(indoc! {"
24686 def f() -> list[str]:
24687 ˇ
24688 "});
24689 cx.update_editor(|editor, window, cx| {
24690 editor.handle_input("a", window, cx);
24691 });
24692 cx.assert_editor_state(indoc! {"
24693 def f() -> list[str]:
24694 aˇ
24695 "});
24696
24697 // test does not outdent on typing : after case keyword
24698 cx.set_state(indoc! {"
24699 match 1:
24700 caseˇ
24701 "});
24702 cx.update_editor(|editor, window, cx| {
24703 editor.handle_input(":", window, cx);
24704 });
24705 cx.assert_editor_state(indoc! {"
24706 match 1:
24707 case:ˇ
24708 "});
24709}
24710
24711#[gpui::test]
24712async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24713 init_test(cx, |_| {});
24714 update_test_language_settings(cx, |settings| {
24715 settings.defaults.extend_comment_on_newline = Some(false);
24716 });
24717 let mut cx = EditorTestContext::new(cx).await;
24718 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24719 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24720
24721 // test correct indent after newline on comment
24722 cx.set_state(indoc! {"
24723 # COMMENT:ˇ
24724 "});
24725 cx.update_editor(|editor, window, cx| {
24726 editor.newline(&Newline, window, cx);
24727 });
24728 cx.assert_editor_state(indoc! {"
24729 # COMMENT:
24730 ˇ
24731 "});
24732
24733 // test correct indent after newline in brackets
24734 cx.set_state(indoc! {"
24735 {ˇ}
24736 "});
24737 cx.update_editor(|editor, window, cx| {
24738 editor.newline(&Newline, window, cx);
24739 });
24740 cx.run_until_parked();
24741 cx.assert_editor_state(indoc! {"
24742 {
24743 ˇ
24744 }
24745 "});
24746
24747 cx.set_state(indoc! {"
24748 (ˇ)
24749 "});
24750 cx.update_editor(|editor, window, cx| {
24751 editor.newline(&Newline, window, cx);
24752 });
24753 cx.run_until_parked();
24754 cx.assert_editor_state(indoc! {"
24755 (
24756 ˇ
24757 )
24758 "});
24759
24760 // do not indent after empty lists or dictionaries
24761 cx.set_state(indoc! {"
24762 a = []ˇ
24763 "});
24764 cx.update_editor(|editor, window, cx| {
24765 editor.newline(&Newline, window, cx);
24766 });
24767 cx.run_until_parked();
24768 cx.assert_editor_state(indoc! {"
24769 a = []
24770 ˇ
24771 "});
24772}
24773
24774#[gpui::test]
24775async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24776 init_test(cx, |_| {});
24777
24778 let mut cx = EditorTestContext::new(cx).await;
24779 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24780 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24781
24782 // test cursor move to start of each line on tab
24783 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24784 cx.set_state(indoc! {"
24785 function main() {
24786 ˇ for item in $items; do
24787 ˇ while [ -n \"$item\" ]; do
24788 ˇ if [ \"$value\" -gt 10 ]; then
24789 ˇ continue
24790 ˇ elif [ \"$value\" -lt 0 ]; then
24791 ˇ break
24792 ˇ else
24793 ˇ echo \"$item\"
24794 ˇ fi
24795 ˇ done
24796 ˇ done
24797 ˇ}
24798 "});
24799 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24800 cx.assert_editor_state(indoc! {"
24801 function main() {
24802 ˇfor item in $items; do
24803 ˇwhile [ -n \"$item\" ]; do
24804 ˇif [ \"$value\" -gt 10 ]; then
24805 ˇcontinue
24806 ˇelif [ \"$value\" -lt 0 ]; then
24807 ˇbreak
24808 ˇelse
24809 ˇecho \"$item\"
24810 ˇfi
24811 ˇdone
24812 ˇdone
24813 ˇ}
24814 "});
24815 // test relative indent is preserved when tab
24816 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24817 cx.assert_editor_state(indoc! {"
24818 function main() {
24819 ˇfor item in $items; do
24820 ˇwhile [ -n \"$item\" ]; do
24821 ˇif [ \"$value\" -gt 10 ]; then
24822 ˇcontinue
24823 ˇelif [ \"$value\" -lt 0 ]; then
24824 ˇbreak
24825 ˇelse
24826 ˇecho \"$item\"
24827 ˇfi
24828 ˇdone
24829 ˇdone
24830 ˇ}
24831 "});
24832
24833 // test cursor move to start of each line on tab
24834 // for `case` statement with patterns
24835 cx.set_state(indoc! {"
24836 function handle() {
24837 ˇ case \"$1\" in
24838 ˇ start)
24839 ˇ echo \"a\"
24840 ˇ ;;
24841 ˇ stop)
24842 ˇ echo \"b\"
24843 ˇ ;;
24844 ˇ *)
24845 ˇ echo \"c\"
24846 ˇ ;;
24847 ˇ esac
24848 ˇ}
24849 "});
24850 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24851 cx.assert_editor_state(indoc! {"
24852 function handle() {
24853 ˇcase \"$1\" in
24854 ˇstart)
24855 ˇecho \"a\"
24856 ˇ;;
24857 ˇstop)
24858 ˇecho \"b\"
24859 ˇ;;
24860 ˇ*)
24861 ˇecho \"c\"
24862 ˇ;;
24863 ˇesac
24864 ˇ}
24865 "});
24866}
24867
24868#[gpui::test]
24869async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24870 init_test(cx, |_| {});
24871
24872 let mut cx = EditorTestContext::new(cx).await;
24873 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24874 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24875
24876 // test indents on comment insert
24877 cx.set_state(indoc! {"
24878 function main() {
24879 ˇ for item in $items; do
24880 ˇ while [ -n \"$item\" ]; do
24881 ˇ if [ \"$value\" -gt 10 ]; then
24882 ˇ continue
24883 ˇ elif [ \"$value\" -lt 0 ]; then
24884 ˇ break
24885 ˇ else
24886 ˇ echo \"$item\"
24887 ˇ fi
24888 ˇ done
24889 ˇ done
24890 ˇ}
24891 "});
24892 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24893 cx.assert_editor_state(indoc! {"
24894 function main() {
24895 #ˇ for item in $items; do
24896 #ˇ while [ -n \"$item\" ]; do
24897 #ˇ if [ \"$value\" -gt 10 ]; then
24898 #ˇ continue
24899 #ˇ elif [ \"$value\" -lt 0 ]; then
24900 #ˇ break
24901 #ˇ else
24902 #ˇ echo \"$item\"
24903 #ˇ fi
24904 #ˇ done
24905 #ˇ done
24906 #ˇ}
24907 "});
24908}
24909
24910#[gpui::test]
24911async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24912 init_test(cx, |_| {});
24913
24914 let mut cx = EditorTestContext::new(cx).await;
24915 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24916 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24917
24918 // test `else` auto outdents when typed inside `if` block
24919 cx.set_state(indoc! {"
24920 if [ \"$1\" = \"test\" ]; then
24921 echo \"foo bar\"
24922 ˇ
24923 "});
24924 cx.update_editor(|editor, window, cx| {
24925 editor.handle_input("else", window, cx);
24926 });
24927 cx.assert_editor_state(indoc! {"
24928 if [ \"$1\" = \"test\" ]; then
24929 echo \"foo bar\"
24930 elseˇ
24931 "});
24932
24933 // test `elif` auto outdents when typed inside `if` block
24934 cx.set_state(indoc! {"
24935 if [ \"$1\" = \"test\" ]; then
24936 echo \"foo bar\"
24937 ˇ
24938 "});
24939 cx.update_editor(|editor, window, cx| {
24940 editor.handle_input("elif", window, cx);
24941 });
24942 cx.assert_editor_state(indoc! {"
24943 if [ \"$1\" = \"test\" ]; then
24944 echo \"foo bar\"
24945 elifˇ
24946 "});
24947
24948 // test `fi` auto outdents when typed inside `else` block
24949 cx.set_state(indoc! {"
24950 if [ \"$1\" = \"test\" ]; then
24951 echo \"foo bar\"
24952 else
24953 echo \"bar baz\"
24954 ˇ
24955 "});
24956 cx.update_editor(|editor, window, cx| {
24957 editor.handle_input("fi", window, cx);
24958 });
24959 cx.assert_editor_state(indoc! {"
24960 if [ \"$1\" = \"test\" ]; then
24961 echo \"foo bar\"
24962 else
24963 echo \"bar baz\"
24964 fiˇ
24965 "});
24966
24967 // test `done` auto outdents when typed inside `while` block
24968 cx.set_state(indoc! {"
24969 while read line; do
24970 echo \"$line\"
24971 ˇ
24972 "});
24973 cx.update_editor(|editor, window, cx| {
24974 editor.handle_input("done", window, cx);
24975 });
24976 cx.assert_editor_state(indoc! {"
24977 while read line; do
24978 echo \"$line\"
24979 doneˇ
24980 "});
24981
24982 // test `done` auto outdents when typed inside `for` block
24983 cx.set_state(indoc! {"
24984 for file in *.txt; do
24985 cat \"$file\"
24986 ˇ
24987 "});
24988 cx.update_editor(|editor, window, cx| {
24989 editor.handle_input("done", window, cx);
24990 });
24991 cx.assert_editor_state(indoc! {"
24992 for file in *.txt; do
24993 cat \"$file\"
24994 doneˇ
24995 "});
24996
24997 // test `esac` auto outdents when typed inside `case` block
24998 cx.set_state(indoc! {"
24999 case \"$1\" in
25000 start)
25001 echo \"foo bar\"
25002 ;;
25003 stop)
25004 echo \"bar baz\"
25005 ;;
25006 ˇ
25007 "});
25008 cx.update_editor(|editor, window, cx| {
25009 editor.handle_input("esac", window, cx);
25010 });
25011 cx.assert_editor_state(indoc! {"
25012 case \"$1\" in
25013 start)
25014 echo \"foo bar\"
25015 ;;
25016 stop)
25017 echo \"bar baz\"
25018 ;;
25019 esacˇ
25020 "});
25021
25022 // test `*)` auto outdents when typed inside `case` block
25023 cx.set_state(indoc! {"
25024 case \"$1\" in
25025 start)
25026 echo \"foo bar\"
25027 ;;
25028 ˇ
25029 "});
25030 cx.update_editor(|editor, window, cx| {
25031 editor.handle_input("*)", window, cx);
25032 });
25033 cx.assert_editor_state(indoc! {"
25034 case \"$1\" in
25035 start)
25036 echo \"foo bar\"
25037 ;;
25038 *)ˇ
25039 "});
25040
25041 // test `fi` outdents to correct level with nested if blocks
25042 cx.set_state(indoc! {"
25043 if [ \"$1\" = \"test\" ]; then
25044 echo \"outer if\"
25045 if [ \"$2\" = \"debug\" ]; then
25046 echo \"inner if\"
25047 ˇ
25048 "});
25049 cx.update_editor(|editor, window, cx| {
25050 editor.handle_input("fi", window, cx);
25051 });
25052 cx.assert_editor_state(indoc! {"
25053 if [ \"$1\" = \"test\" ]; then
25054 echo \"outer if\"
25055 if [ \"$2\" = \"debug\" ]; then
25056 echo \"inner if\"
25057 fiˇ
25058 "});
25059}
25060
25061#[gpui::test]
25062async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25063 init_test(cx, |_| {});
25064 update_test_language_settings(cx, |settings| {
25065 settings.defaults.extend_comment_on_newline = Some(false);
25066 });
25067 let mut cx = EditorTestContext::new(cx).await;
25068 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25069 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25070
25071 // test correct indent after newline on comment
25072 cx.set_state(indoc! {"
25073 # COMMENT:ˇ
25074 "});
25075 cx.update_editor(|editor, window, cx| {
25076 editor.newline(&Newline, window, cx);
25077 });
25078 cx.assert_editor_state(indoc! {"
25079 # COMMENT:
25080 ˇ
25081 "});
25082
25083 // test correct indent after newline after `then`
25084 cx.set_state(indoc! {"
25085
25086 if [ \"$1\" = \"test\" ]; thenˇ
25087 "});
25088 cx.update_editor(|editor, window, cx| {
25089 editor.newline(&Newline, window, cx);
25090 });
25091 cx.run_until_parked();
25092 cx.assert_editor_state(indoc! {"
25093
25094 if [ \"$1\" = \"test\" ]; then
25095 ˇ
25096 "});
25097
25098 // test correct indent after newline after `else`
25099 cx.set_state(indoc! {"
25100 if [ \"$1\" = \"test\" ]; then
25101 elseˇ
25102 "});
25103 cx.update_editor(|editor, window, cx| {
25104 editor.newline(&Newline, window, cx);
25105 });
25106 cx.run_until_parked();
25107 cx.assert_editor_state(indoc! {"
25108 if [ \"$1\" = \"test\" ]; then
25109 else
25110 ˇ
25111 "});
25112
25113 // test correct indent after newline after `elif`
25114 cx.set_state(indoc! {"
25115 if [ \"$1\" = \"test\" ]; then
25116 elifˇ
25117 "});
25118 cx.update_editor(|editor, window, cx| {
25119 editor.newline(&Newline, window, cx);
25120 });
25121 cx.run_until_parked();
25122 cx.assert_editor_state(indoc! {"
25123 if [ \"$1\" = \"test\" ]; then
25124 elif
25125 ˇ
25126 "});
25127
25128 // test correct indent after newline after `do`
25129 cx.set_state(indoc! {"
25130 for file in *.txt; doˇ
25131 "});
25132 cx.update_editor(|editor, window, cx| {
25133 editor.newline(&Newline, window, cx);
25134 });
25135 cx.run_until_parked();
25136 cx.assert_editor_state(indoc! {"
25137 for file in *.txt; do
25138 ˇ
25139 "});
25140
25141 // test correct indent after newline after case pattern
25142 cx.set_state(indoc! {"
25143 case \"$1\" in
25144 start)ˇ
25145 "});
25146 cx.update_editor(|editor, window, cx| {
25147 editor.newline(&Newline, window, cx);
25148 });
25149 cx.run_until_parked();
25150 cx.assert_editor_state(indoc! {"
25151 case \"$1\" in
25152 start)
25153 ˇ
25154 "});
25155
25156 // test correct indent after newline after case pattern
25157 cx.set_state(indoc! {"
25158 case \"$1\" in
25159 start)
25160 ;;
25161 *)ˇ
25162 "});
25163 cx.update_editor(|editor, window, cx| {
25164 editor.newline(&Newline, window, cx);
25165 });
25166 cx.run_until_parked();
25167 cx.assert_editor_state(indoc! {"
25168 case \"$1\" in
25169 start)
25170 ;;
25171 *)
25172 ˇ
25173 "});
25174
25175 // test correct indent after newline after function opening brace
25176 cx.set_state(indoc! {"
25177 function test() {ˇ}
25178 "});
25179 cx.update_editor(|editor, window, cx| {
25180 editor.newline(&Newline, window, cx);
25181 });
25182 cx.run_until_parked();
25183 cx.assert_editor_state(indoc! {"
25184 function test() {
25185 ˇ
25186 }
25187 "});
25188
25189 // test no extra indent after semicolon on same line
25190 cx.set_state(indoc! {"
25191 echo \"test\";ˇ
25192 "});
25193 cx.update_editor(|editor, window, cx| {
25194 editor.newline(&Newline, window, cx);
25195 });
25196 cx.run_until_parked();
25197 cx.assert_editor_state(indoc! {"
25198 echo \"test\";
25199 ˇ
25200 "});
25201}
25202
25203fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25204 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25205 point..point
25206}
25207
25208#[track_caller]
25209fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25210 let (text, ranges) = marked_text_ranges(marked_text, true);
25211 assert_eq!(editor.text(cx), text);
25212 assert_eq!(
25213 editor.selections.ranges(cx),
25214 ranges,
25215 "Assert selections are {}",
25216 marked_text
25217 );
25218}
25219
25220pub fn handle_signature_help_request(
25221 cx: &mut EditorLspTestContext,
25222 mocked_response: lsp::SignatureHelp,
25223) -> impl Future<Output = ()> + use<> {
25224 let mut request =
25225 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25226 let mocked_response = mocked_response.clone();
25227 async move { Ok(Some(mocked_response)) }
25228 });
25229
25230 async move {
25231 request.next().await;
25232 }
25233}
25234
25235#[track_caller]
25236pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25237 cx.update_editor(|editor, _, _| {
25238 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25239 let entries = menu.entries.borrow();
25240 let entries = entries
25241 .iter()
25242 .map(|entry| entry.string.as_str())
25243 .collect::<Vec<_>>();
25244 assert_eq!(entries, expected);
25245 } else {
25246 panic!("Expected completions menu");
25247 }
25248 });
25249}
25250
25251/// Handle completion request passing a marked string specifying where the completion
25252/// should be triggered from using '|' character, what range should be replaced, and what completions
25253/// should be returned using '<' and '>' to delimit the range.
25254///
25255/// Also see `handle_completion_request_with_insert_and_replace`.
25256#[track_caller]
25257pub fn handle_completion_request(
25258 marked_string: &str,
25259 completions: Vec<&'static str>,
25260 is_incomplete: bool,
25261 counter: Arc<AtomicUsize>,
25262 cx: &mut EditorLspTestContext,
25263) -> impl Future<Output = ()> {
25264 let complete_from_marker: TextRangeMarker = '|'.into();
25265 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25266 let (_, mut marked_ranges) = marked_text_ranges_by(
25267 marked_string,
25268 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25269 );
25270
25271 let complete_from_position =
25272 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25273 let replace_range =
25274 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25275
25276 let mut request =
25277 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25278 let completions = completions.clone();
25279 counter.fetch_add(1, atomic::Ordering::Release);
25280 async move {
25281 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25282 assert_eq!(
25283 params.text_document_position.position,
25284 complete_from_position
25285 );
25286 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25287 is_incomplete,
25288 item_defaults: None,
25289 items: completions
25290 .iter()
25291 .map(|completion_text| lsp::CompletionItem {
25292 label: completion_text.to_string(),
25293 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25294 range: replace_range,
25295 new_text: completion_text.to_string(),
25296 })),
25297 ..Default::default()
25298 })
25299 .collect(),
25300 })))
25301 }
25302 });
25303
25304 async move {
25305 request.next().await;
25306 }
25307}
25308
25309/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25310/// given instead, which also contains an `insert` range.
25311///
25312/// This function uses markers to define ranges:
25313/// - `|` marks the cursor position
25314/// - `<>` marks the replace range
25315/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25316pub fn handle_completion_request_with_insert_and_replace(
25317 cx: &mut EditorLspTestContext,
25318 marked_string: &str,
25319 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25320 counter: Arc<AtomicUsize>,
25321) -> impl Future<Output = ()> {
25322 let complete_from_marker: TextRangeMarker = '|'.into();
25323 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25324 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25325
25326 let (_, mut marked_ranges) = marked_text_ranges_by(
25327 marked_string,
25328 vec![
25329 complete_from_marker.clone(),
25330 replace_range_marker.clone(),
25331 insert_range_marker.clone(),
25332 ],
25333 );
25334
25335 let complete_from_position =
25336 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25337 let replace_range =
25338 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25339
25340 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25341 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25342 _ => lsp::Range {
25343 start: replace_range.start,
25344 end: complete_from_position,
25345 },
25346 };
25347
25348 let mut request =
25349 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25350 let completions = completions.clone();
25351 counter.fetch_add(1, atomic::Ordering::Release);
25352 async move {
25353 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25354 assert_eq!(
25355 params.text_document_position.position, complete_from_position,
25356 "marker `|` position doesn't match",
25357 );
25358 Ok(Some(lsp::CompletionResponse::Array(
25359 completions
25360 .iter()
25361 .map(|(label, new_text)| lsp::CompletionItem {
25362 label: label.to_string(),
25363 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25364 lsp::InsertReplaceEdit {
25365 insert: insert_range,
25366 replace: replace_range,
25367 new_text: new_text.to_string(),
25368 },
25369 )),
25370 ..Default::default()
25371 })
25372 .collect(),
25373 )))
25374 }
25375 });
25376
25377 async move {
25378 request.next().await;
25379 }
25380}
25381
25382fn handle_resolve_completion_request(
25383 cx: &mut EditorLspTestContext,
25384 edits: Option<Vec<(&'static str, &'static str)>>,
25385) -> impl Future<Output = ()> {
25386 let edits = edits.map(|edits| {
25387 edits
25388 .iter()
25389 .map(|(marked_string, new_text)| {
25390 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25391 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25392 lsp::TextEdit::new(replace_range, new_text.to_string())
25393 })
25394 .collect::<Vec<_>>()
25395 });
25396
25397 let mut request =
25398 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25399 let edits = edits.clone();
25400 async move {
25401 Ok(lsp::CompletionItem {
25402 additional_text_edits: edits,
25403 ..Default::default()
25404 })
25405 }
25406 });
25407
25408 async move {
25409 request.next().await;
25410 }
25411}
25412
25413pub(crate) fn update_test_language_settings(
25414 cx: &mut TestAppContext,
25415 f: impl Fn(&mut AllLanguageSettingsContent),
25416) {
25417 cx.update(|cx| {
25418 SettingsStore::update_global(cx, |store, cx| {
25419 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25420 });
25421 });
25422}
25423
25424pub(crate) fn update_test_project_settings(
25425 cx: &mut TestAppContext,
25426 f: impl Fn(&mut ProjectSettingsContent),
25427) {
25428 cx.update(|cx| {
25429 SettingsStore::update_global(cx, |store, cx| {
25430 store.update_user_settings(cx, |settings| f(&mut settings.project));
25431 });
25432 });
25433}
25434
25435pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25436 cx.update(|cx| {
25437 assets::Assets.load_test_fonts(cx);
25438 let store = SettingsStore::test(cx);
25439 cx.set_global(store);
25440 theme::init(theme::LoadThemes::JustBase, cx);
25441 release_channel::init(SemanticVersion::default(), cx);
25442 client::init_settings(cx);
25443 language::init(cx);
25444 Project::init_settings(cx);
25445 workspace::init_settings(cx);
25446 crate::init(cx);
25447 });
25448 zlog::init_test();
25449 update_test_language_settings(cx, f);
25450}
25451
25452#[track_caller]
25453fn assert_hunk_revert(
25454 not_reverted_text_with_selections: &str,
25455 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25456 expected_reverted_text_with_selections: &str,
25457 base_text: &str,
25458 cx: &mut EditorLspTestContext,
25459) {
25460 cx.set_state(not_reverted_text_with_selections);
25461 cx.set_head_text(base_text);
25462 cx.executor().run_until_parked();
25463
25464 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25465 let snapshot = editor.snapshot(window, cx);
25466 let reverted_hunk_statuses = snapshot
25467 .buffer_snapshot()
25468 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
25469 .map(|hunk| hunk.status().kind)
25470 .collect::<Vec<_>>();
25471
25472 editor.git_restore(&Default::default(), window, cx);
25473 reverted_hunk_statuses
25474 });
25475 cx.executor().run_until_parked();
25476 cx.assert_editor_state(expected_reverted_text_with_selections);
25477 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25478}
25479
25480#[gpui::test(iterations = 10)]
25481async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25482 init_test(cx, |_| {});
25483
25484 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25485 let counter = diagnostic_requests.clone();
25486
25487 let fs = FakeFs::new(cx.executor());
25488 fs.insert_tree(
25489 path!("/a"),
25490 json!({
25491 "first.rs": "fn main() { let a = 5; }",
25492 "second.rs": "// Test file",
25493 }),
25494 )
25495 .await;
25496
25497 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25498 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25499 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25500
25501 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25502 language_registry.add(rust_lang());
25503 let mut fake_servers = language_registry.register_fake_lsp(
25504 "Rust",
25505 FakeLspAdapter {
25506 capabilities: lsp::ServerCapabilities {
25507 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25508 lsp::DiagnosticOptions {
25509 identifier: None,
25510 inter_file_dependencies: true,
25511 workspace_diagnostics: true,
25512 work_done_progress_options: Default::default(),
25513 },
25514 )),
25515 ..Default::default()
25516 },
25517 ..Default::default()
25518 },
25519 );
25520
25521 let editor = workspace
25522 .update(cx, |workspace, window, cx| {
25523 workspace.open_abs_path(
25524 PathBuf::from(path!("/a/first.rs")),
25525 OpenOptions::default(),
25526 window,
25527 cx,
25528 )
25529 })
25530 .unwrap()
25531 .await
25532 .unwrap()
25533 .downcast::<Editor>()
25534 .unwrap();
25535 let fake_server = fake_servers.next().await.unwrap();
25536 let server_id = fake_server.server.server_id();
25537 let mut first_request = fake_server
25538 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25539 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25540 let result_id = Some(new_result_id.to_string());
25541 assert_eq!(
25542 params.text_document.uri,
25543 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25544 );
25545 async move {
25546 Ok(lsp::DocumentDiagnosticReportResult::Report(
25547 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25548 related_documents: None,
25549 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25550 items: Vec::new(),
25551 result_id,
25552 },
25553 }),
25554 ))
25555 }
25556 });
25557
25558 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25559 project.update(cx, |project, cx| {
25560 let buffer_id = editor
25561 .read(cx)
25562 .buffer()
25563 .read(cx)
25564 .as_singleton()
25565 .expect("created a singleton buffer")
25566 .read(cx)
25567 .remote_id();
25568 let buffer_result_id = project
25569 .lsp_store()
25570 .read(cx)
25571 .result_id(server_id, buffer_id, cx);
25572 assert_eq!(expected, buffer_result_id);
25573 });
25574 };
25575
25576 ensure_result_id(None, cx);
25577 cx.executor().advance_clock(Duration::from_millis(60));
25578 cx.executor().run_until_parked();
25579 assert_eq!(
25580 diagnostic_requests.load(atomic::Ordering::Acquire),
25581 1,
25582 "Opening file should trigger diagnostic request"
25583 );
25584 first_request
25585 .next()
25586 .await
25587 .expect("should have sent the first diagnostics pull request");
25588 ensure_result_id(Some("1".to_string()), cx);
25589
25590 // Editing should trigger diagnostics
25591 editor.update_in(cx, |editor, window, cx| {
25592 editor.handle_input("2", window, cx)
25593 });
25594 cx.executor().advance_clock(Duration::from_millis(60));
25595 cx.executor().run_until_parked();
25596 assert_eq!(
25597 diagnostic_requests.load(atomic::Ordering::Acquire),
25598 2,
25599 "Editing should trigger diagnostic request"
25600 );
25601 ensure_result_id(Some("2".to_string()), cx);
25602
25603 // Moving cursor should not trigger diagnostic request
25604 editor.update_in(cx, |editor, window, cx| {
25605 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25606 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25607 });
25608 });
25609 cx.executor().advance_clock(Duration::from_millis(60));
25610 cx.executor().run_until_parked();
25611 assert_eq!(
25612 diagnostic_requests.load(atomic::Ordering::Acquire),
25613 2,
25614 "Cursor movement should not trigger diagnostic request"
25615 );
25616 ensure_result_id(Some("2".to_string()), cx);
25617 // Multiple rapid edits should be debounced
25618 for _ in 0..5 {
25619 editor.update_in(cx, |editor, window, cx| {
25620 editor.handle_input("x", window, cx)
25621 });
25622 }
25623 cx.executor().advance_clock(Duration::from_millis(60));
25624 cx.executor().run_until_parked();
25625
25626 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25627 assert!(
25628 final_requests <= 4,
25629 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25630 );
25631 ensure_result_id(Some(final_requests.to_string()), cx);
25632}
25633
25634#[gpui::test]
25635async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25636 // Regression test for issue #11671
25637 // Previously, adding a cursor after moving multiple cursors would reset
25638 // the cursor count instead of adding to the existing cursors.
25639 init_test(cx, |_| {});
25640 let mut cx = EditorTestContext::new(cx).await;
25641
25642 // Create a simple buffer with cursor at start
25643 cx.set_state(indoc! {"
25644 ˇaaaa
25645 bbbb
25646 cccc
25647 dddd
25648 eeee
25649 ffff
25650 gggg
25651 hhhh"});
25652
25653 // Add 2 cursors below (so we have 3 total)
25654 cx.update_editor(|editor, window, cx| {
25655 editor.add_selection_below(&Default::default(), window, cx);
25656 editor.add_selection_below(&Default::default(), window, cx);
25657 });
25658
25659 // Verify we have 3 cursors
25660 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25661 assert_eq!(
25662 initial_count, 3,
25663 "Should have 3 cursors after adding 2 below"
25664 );
25665
25666 // Move down one line
25667 cx.update_editor(|editor, window, cx| {
25668 editor.move_down(&MoveDown, window, cx);
25669 });
25670
25671 // Add another cursor below
25672 cx.update_editor(|editor, window, cx| {
25673 editor.add_selection_below(&Default::default(), window, cx);
25674 });
25675
25676 // Should now have 4 cursors (3 original + 1 new)
25677 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25678 assert_eq!(
25679 final_count, 4,
25680 "Should have 4 cursors after moving and adding another"
25681 );
25682}
25683
25684#[gpui::test]
25685async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
25686 init_test(cx, |_| {});
25687
25688 let mut cx = EditorTestContext::new(cx).await;
25689
25690 cx.set_state(indoc!(
25691 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
25692 Second line here"#
25693 ));
25694
25695 cx.update_editor(|editor, window, cx| {
25696 // Enable soft wrapping with a narrow width to force soft wrapping and
25697 // confirm that more than 2 rows are being displayed.
25698 editor.set_wrap_width(Some(100.0.into()), cx);
25699 assert!(editor.display_text(cx).lines().count() > 2);
25700
25701 editor.add_selection_below(
25702 &AddSelectionBelow {
25703 skip_soft_wrap: true,
25704 },
25705 window,
25706 cx,
25707 );
25708
25709 assert_eq!(
25710 editor.selections.display_ranges(cx),
25711 &[
25712 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
25713 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
25714 ]
25715 );
25716
25717 editor.add_selection_above(
25718 &AddSelectionAbove {
25719 skip_soft_wrap: true,
25720 },
25721 window,
25722 cx,
25723 );
25724
25725 assert_eq!(
25726 editor.selections.display_ranges(cx),
25727 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
25728 );
25729
25730 editor.add_selection_below(
25731 &AddSelectionBelow {
25732 skip_soft_wrap: false,
25733 },
25734 window,
25735 cx,
25736 );
25737
25738 assert_eq!(
25739 editor.selections.display_ranges(cx),
25740 &[
25741 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
25742 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
25743 ]
25744 );
25745
25746 editor.add_selection_above(
25747 &AddSelectionAbove {
25748 skip_soft_wrap: false,
25749 },
25750 window,
25751 cx,
25752 );
25753
25754 assert_eq!(
25755 editor.selections.display_ranges(cx),
25756 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
25757 );
25758 });
25759}
25760
25761#[gpui::test(iterations = 10)]
25762async fn test_document_colors(cx: &mut TestAppContext) {
25763 let expected_color = Rgba {
25764 r: 0.33,
25765 g: 0.33,
25766 b: 0.33,
25767 a: 0.33,
25768 };
25769
25770 init_test(cx, |_| {});
25771
25772 let fs = FakeFs::new(cx.executor());
25773 fs.insert_tree(
25774 path!("/a"),
25775 json!({
25776 "first.rs": "fn main() { let a = 5; }",
25777 }),
25778 )
25779 .await;
25780
25781 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25782 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25783 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25784
25785 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25786 language_registry.add(rust_lang());
25787 let mut fake_servers = language_registry.register_fake_lsp(
25788 "Rust",
25789 FakeLspAdapter {
25790 capabilities: lsp::ServerCapabilities {
25791 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25792 ..lsp::ServerCapabilities::default()
25793 },
25794 name: "rust-analyzer",
25795 ..FakeLspAdapter::default()
25796 },
25797 );
25798 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25799 "Rust",
25800 FakeLspAdapter {
25801 capabilities: lsp::ServerCapabilities {
25802 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25803 ..lsp::ServerCapabilities::default()
25804 },
25805 name: "not-rust-analyzer",
25806 ..FakeLspAdapter::default()
25807 },
25808 );
25809
25810 let editor = workspace
25811 .update(cx, |workspace, window, cx| {
25812 workspace.open_abs_path(
25813 PathBuf::from(path!("/a/first.rs")),
25814 OpenOptions::default(),
25815 window,
25816 cx,
25817 )
25818 })
25819 .unwrap()
25820 .await
25821 .unwrap()
25822 .downcast::<Editor>()
25823 .unwrap();
25824 let fake_language_server = fake_servers.next().await.unwrap();
25825 let fake_language_server_without_capabilities =
25826 fake_servers_without_capabilities.next().await.unwrap();
25827 let requests_made = Arc::new(AtomicUsize::new(0));
25828 let closure_requests_made = Arc::clone(&requests_made);
25829 let mut color_request_handle = fake_language_server
25830 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25831 let requests_made = Arc::clone(&closure_requests_made);
25832 async move {
25833 assert_eq!(
25834 params.text_document.uri,
25835 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25836 );
25837 requests_made.fetch_add(1, atomic::Ordering::Release);
25838 Ok(vec![
25839 lsp::ColorInformation {
25840 range: lsp::Range {
25841 start: lsp::Position {
25842 line: 0,
25843 character: 0,
25844 },
25845 end: lsp::Position {
25846 line: 0,
25847 character: 1,
25848 },
25849 },
25850 color: lsp::Color {
25851 red: 0.33,
25852 green: 0.33,
25853 blue: 0.33,
25854 alpha: 0.33,
25855 },
25856 },
25857 lsp::ColorInformation {
25858 range: lsp::Range {
25859 start: lsp::Position {
25860 line: 0,
25861 character: 0,
25862 },
25863 end: lsp::Position {
25864 line: 0,
25865 character: 1,
25866 },
25867 },
25868 color: lsp::Color {
25869 red: 0.33,
25870 green: 0.33,
25871 blue: 0.33,
25872 alpha: 0.33,
25873 },
25874 },
25875 ])
25876 }
25877 });
25878
25879 let _handle = fake_language_server_without_capabilities
25880 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25881 panic!("Should not be called");
25882 });
25883 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
25884 color_request_handle.next().await.unwrap();
25885 cx.run_until_parked();
25886 assert_eq!(
25887 1,
25888 requests_made.load(atomic::Ordering::Acquire),
25889 "Should query for colors once per editor open"
25890 );
25891 editor.update_in(cx, |editor, _, cx| {
25892 assert_eq!(
25893 vec![expected_color],
25894 extract_color_inlays(editor, cx),
25895 "Should have an initial inlay"
25896 );
25897 });
25898
25899 // opening another file in a split should not influence the LSP query counter
25900 workspace
25901 .update(cx, |workspace, window, cx| {
25902 assert_eq!(
25903 workspace.panes().len(),
25904 1,
25905 "Should have one pane with one editor"
25906 );
25907 workspace.move_item_to_pane_in_direction(
25908 &MoveItemToPaneInDirection {
25909 direction: SplitDirection::Right,
25910 focus: false,
25911 clone: true,
25912 },
25913 window,
25914 cx,
25915 );
25916 })
25917 .unwrap();
25918 cx.run_until_parked();
25919 workspace
25920 .update(cx, |workspace, _, cx| {
25921 let panes = workspace.panes();
25922 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25923 for pane in panes {
25924 let editor = pane
25925 .read(cx)
25926 .active_item()
25927 .and_then(|item| item.downcast::<Editor>())
25928 .expect("Should have opened an editor in each split");
25929 let editor_file = editor
25930 .read(cx)
25931 .buffer()
25932 .read(cx)
25933 .as_singleton()
25934 .expect("test deals with singleton buffers")
25935 .read(cx)
25936 .file()
25937 .expect("test buffese should have a file")
25938 .path();
25939 assert_eq!(
25940 editor_file.as_ref(),
25941 rel_path("first.rs"),
25942 "Both editors should be opened for the same file"
25943 )
25944 }
25945 })
25946 .unwrap();
25947
25948 cx.executor().advance_clock(Duration::from_millis(500));
25949 let save = editor.update_in(cx, |editor, window, cx| {
25950 editor.move_to_end(&MoveToEnd, window, cx);
25951 editor.handle_input("dirty", window, cx);
25952 editor.save(
25953 SaveOptions {
25954 format: true,
25955 autosave: true,
25956 },
25957 project.clone(),
25958 window,
25959 cx,
25960 )
25961 });
25962 save.await.unwrap();
25963
25964 color_request_handle.next().await.unwrap();
25965 cx.run_until_parked();
25966 assert_eq!(
25967 2,
25968 requests_made.load(atomic::Ordering::Acquire),
25969 "Should query for colors once per save (deduplicated) and once per formatting after save"
25970 );
25971
25972 drop(editor);
25973 let close = workspace
25974 .update(cx, |workspace, window, cx| {
25975 workspace.active_pane().update(cx, |pane, cx| {
25976 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25977 })
25978 })
25979 .unwrap();
25980 close.await.unwrap();
25981 let close = workspace
25982 .update(cx, |workspace, window, cx| {
25983 workspace.active_pane().update(cx, |pane, cx| {
25984 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25985 })
25986 })
25987 .unwrap();
25988 close.await.unwrap();
25989 assert_eq!(
25990 2,
25991 requests_made.load(atomic::Ordering::Acquire),
25992 "After saving and closing all editors, no extra requests should be made"
25993 );
25994 workspace
25995 .update(cx, |workspace, _, cx| {
25996 assert!(
25997 workspace.active_item(cx).is_none(),
25998 "Should close all editors"
25999 )
26000 })
26001 .unwrap();
26002
26003 workspace
26004 .update(cx, |workspace, window, cx| {
26005 workspace.active_pane().update(cx, |pane, cx| {
26006 pane.navigate_backward(&workspace::GoBack, window, cx);
26007 })
26008 })
26009 .unwrap();
26010 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26011 cx.run_until_parked();
26012 let editor = workspace
26013 .update(cx, |workspace, _, cx| {
26014 workspace
26015 .active_item(cx)
26016 .expect("Should have reopened the editor again after navigating back")
26017 .downcast::<Editor>()
26018 .expect("Should be an editor")
26019 })
26020 .unwrap();
26021
26022 assert_eq!(
26023 2,
26024 requests_made.load(atomic::Ordering::Acquire),
26025 "Cache should be reused on buffer close and reopen"
26026 );
26027 editor.update(cx, |editor, cx| {
26028 assert_eq!(
26029 vec![expected_color],
26030 extract_color_inlays(editor, cx),
26031 "Should have an initial inlay"
26032 );
26033 });
26034
26035 drop(color_request_handle);
26036 let closure_requests_made = Arc::clone(&requests_made);
26037 let mut empty_color_request_handle = fake_language_server
26038 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26039 let requests_made = Arc::clone(&closure_requests_made);
26040 async move {
26041 assert_eq!(
26042 params.text_document.uri,
26043 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26044 );
26045 requests_made.fetch_add(1, atomic::Ordering::Release);
26046 Ok(Vec::new())
26047 }
26048 });
26049 let save = editor.update_in(cx, |editor, window, cx| {
26050 editor.move_to_end(&MoveToEnd, window, cx);
26051 editor.handle_input("dirty_again", window, cx);
26052 editor.save(
26053 SaveOptions {
26054 format: false,
26055 autosave: true,
26056 },
26057 project.clone(),
26058 window,
26059 cx,
26060 )
26061 });
26062 save.await.unwrap();
26063
26064 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26065 empty_color_request_handle.next().await.unwrap();
26066 cx.run_until_parked();
26067 assert_eq!(
26068 3,
26069 requests_made.load(atomic::Ordering::Acquire),
26070 "Should query for colors once per save only, as formatting was not requested"
26071 );
26072 editor.update(cx, |editor, cx| {
26073 assert_eq!(
26074 Vec::<Rgba>::new(),
26075 extract_color_inlays(editor, cx),
26076 "Should clear all colors when the server returns an empty response"
26077 );
26078 });
26079}
26080
26081#[gpui::test]
26082async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26083 init_test(cx, |_| {});
26084 let (editor, cx) = cx.add_window_view(Editor::single_line);
26085 editor.update_in(cx, |editor, window, cx| {
26086 editor.set_text("oops\n\nwow\n", window, cx)
26087 });
26088 cx.run_until_parked();
26089 editor.update(cx, |editor, cx| {
26090 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26091 });
26092 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
26093 cx.run_until_parked();
26094 editor.update(cx, |editor, cx| {
26095 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26096 });
26097}
26098
26099#[gpui::test]
26100async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26101 init_test(cx, |_| {});
26102
26103 cx.update(|cx| {
26104 register_project_item::<Editor>(cx);
26105 });
26106
26107 let fs = FakeFs::new(cx.executor());
26108 fs.insert_tree("/root1", json!({})).await;
26109 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26110 .await;
26111
26112 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26113 let (workspace, cx) =
26114 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26115
26116 let worktree_id = project.update(cx, |project, cx| {
26117 project.worktrees(cx).next().unwrap().read(cx).id()
26118 });
26119
26120 let handle = workspace
26121 .update_in(cx, |workspace, window, cx| {
26122 let project_path = (worktree_id, rel_path("one.pdf"));
26123 workspace.open_path(project_path, None, true, window, cx)
26124 })
26125 .await
26126 .unwrap();
26127
26128 assert_eq!(
26129 handle.to_any().entity_type(),
26130 TypeId::of::<InvalidBufferView>()
26131 );
26132}
26133
26134#[gpui::test]
26135async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26136 init_test(cx, |_| {});
26137
26138 let language = Arc::new(Language::new(
26139 LanguageConfig::default(),
26140 Some(tree_sitter_rust::LANGUAGE.into()),
26141 ));
26142
26143 // Test hierarchical sibling navigation
26144 let text = r#"
26145 fn outer() {
26146 if condition {
26147 let a = 1;
26148 }
26149 let b = 2;
26150 }
26151
26152 fn another() {
26153 let c = 3;
26154 }
26155 "#;
26156
26157 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
26158 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
26159 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
26160
26161 // Wait for parsing to complete
26162 editor
26163 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
26164 .await;
26165
26166 editor.update_in(cx, |editor, window, cx| {
26167 // Start by selecting "let a = 1;" inside the if block
26168 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26169 s.select_display_ranges([
26170 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
26171 ]);
26172 });
26173
26174 let initial_selection = editor.selections.display_ranges(cx);
26175 assert_eq!(initial_selection.len(), 1, "Should have one selection");
26176
26177 // Test select next sibling - should move up levels to find the next sibling
26178 // Since "let a = 1;" has no siblings in the if block, it should move up
26179 // to find "let b = 2;" which is a sibling of the if block
26180 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26181 let next_selection = editor.selections.display_ranges(cx);
26182
26183 // Should have a selection and it should be different from the initial
26184 assert_eq!(
26185 next_selection.len(),
26186 1,
26187 "Should have one selection after next"
26188 );
26189 assert_ne!(
26190 next_selection[0], initial_selection[0],
26191 "Next sibling selection should be different"
26192 );
26193
26194 // Test hierarchical navigation by going to the end of the current function
26195 // and trying to navigate to the next function
26196 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26197 s.select_display_ranges([
26198 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26199 ]);
26200 });
26201
26202 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26203 let function_next_selection = editor.selections.display_ranges(cx);
26204
26205 // Should move to the next function
26206 assert_eq!(
26207 function_next_selection.len(),
26208 1,
26209 "Should have one selection after function next"
26210 );
26211
26212 // Test select previous sibling navigation
26213 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26214 let prev_selection = editor.selections.display_ranges(cx);
26215
26216 // Should have a selection and it should be different
26217 assert_eq!(
26218 prev_selection.len(),
26219 1,
26220 "Should have one selection after prev"
26221 );
26222 assert_ne!(
26223 prev_selection[0], function_next_selection[0],
26224 "Previous sibling selection should be different from next"
26225 );
26226 });
26227}
26228
26229#[gpui::test]
26230async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26231 init_test(cx, |_| {});
26232
26233 let mut cx = EditorTestContext::new(cx).await;
26234 cx.set_state(
26235 "let ˇvariable = 42;
26236let another = variable + 1;
26237let result = variable * 2;",
26238 );
26239
26240 // Set up document highlights manually (simulating LSP response)
26241 cx.update_editor(|editor, _window, cx| {
26242 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26243
26244 // Create highlights for "variable" occurrences
26245 let highlight_ranges = [
26246 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26247 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26248 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26249 ];
26250
26251 let anchor_ranges: Vec<_> = highlight_ranges
26252 .iter()
26253 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26254 .collect();
26255
26256 editor.highlight_background::<DocumentHighlightRead>(
26257 &anchor_ranges,
26258 |theme| theme.colors().editor_document_highlight_read_background,
26259 cx,
26260 );
26261 });
26262
26263 // Go to next highlight - should move to second "variable"
26264 cx.update_editor(|editor, window, cx| {
26265 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26266 });
26267 cx.assert_editor_state(
26268 "let variable = 42;
26269let another = ˇvariable + 1;
26270let result = variable * 2;",
26271 );
26272
26273 // Go to next highlight - should move to third "variable"
26274 cx.update_editor(|editor, window, cx| {
26275 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26276 });
26277 cx.assert_editor_state(
26278 "let variable = 42;
26279let another = variable + 1;
26280let result = ˇvariable * 2;",
26281 );
26282
26283 // Go to next highlight - should stay at third "variable" (no wrap-around)
26284 cx.update_editor(|editor, window, cx| {
26285 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26286 });
26287 cx.assert_editor_state(
26288 "let variable = 42;
26289let another = variable + 1;
26290let result = ˇvariable * 2;",
26291 );
26292
26293 // Now test going backwards from third position
26294 cx.update_editor(|editor, window, cx| {
26295 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26296 });
26297 cx.assert_editor_state(
26298 "let variable = 42;
26299let another = ˇvariable + 1;
26300let result = variable * 2;",
26301 );
26302
26303 // Go to previous highlight - should move to first "variable"
26304 cx.update_editor(|editor, window, cx| {
26305 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26306 });
26307 cx.assert_editor_state(
26308 "let ˇvariable = 42;
26309let another = variable + 1;
26310let result = variable * 2;",
26311 );
26312
26313 // Go to previous highlight - should stay on first "variable"
26314 cx.update_editor(|editor, window, cx| {
26315 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26316 });
26317 cx.assert_editor_state(
26318 "let ˇvariable = 42;
26319let another = variable + 1;
26320let result = variable * 2;",
26321 );
26322}
26323
26324#[gpui::test]
26325async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26326 cx: &mut gpui::TestAppContext,
26327) {
26328 init_test(cx, |_| {});
26329
26330 let url = "https://zed.dev";
26331
26332 let markdown_language = Arc::new(Language::new(
26333 LanguageConfig {
26334 name: "Markdown".into(),
26335 ..LanguageConfig::default()
26336 },
26337 None,
26338 ));
26339
26340 let mut cx = EditorTestContext::new(cx).await;
26341 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26342 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26343
26344 cx.update_editor(|editor, window, cx| {
26345 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26346 editor.paste(&Paste, window, cx);
26347 });
26348
26349 cx.assert_editor_state(&format!(
26350 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26351 ));
26352}
26353
26354#[gpui::test]
26355async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26356 cx: &mut gpui::TestAppContext,
26357) {
26358 init_test(cx, |_| {});
26359
26360 let url = "https://zed.dev";
26361
26362 let markdown_language = Arc::new(Language::new(
26363 LanguageConfig {
26364 name: "Markdown".into(),
26365 ..LanguageConfig::default()
26366 },
26367 None,
26368 ));
26369
26370 let mut cx = EditorTestContext::new(cx).await;
26371 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26372 cx.set_state(&format!(
26373 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26374 ));
26375
26376 cx.update_editor(|editor, window, cx| {
26377 editor.copy(&Copy, window, cx);
26378 });
26379
26380 cx.set_state(&format!(
26381 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26382 ));
26383
26384 cx.update_editor(|editor, window, cx| {
26385 editor.paste(&Paste, window, cx);
26386 });
26387
26388 cx.assert_editor_state(&format!(
26389 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26390 ));
26391}
26392
26393#[gpui::test]
26394async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26395 cx: &mut gpui::TestAppContext,
26396) {
26397 init_test(cx, |_| {});
26398
26399 let url = "https://zed.dev";
26400
26401 let markdown_language = Arc::new(Language::new(
26402 LanguageConfig {
26403 name: "Markdown".into(),
26404 ..LanguageConfig::default()
26405 },
26406 None,
26407 ));
26408
26409 let mut cx = EditorTestContext::new(cx).await;
26410 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26411 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26412
26413 cx.update_editor(|editor, window, cx| {
26414 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26415 editor.paste(&Paste, window, cx);
26416 });
26417
26418 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26419}
26420
26421#[gpui::test]
26422async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26423 cx: &mut gpui::TestAppContext,
26424) {
26425 init_test(cx, |_| {});
26426
26427 let text = "Awesome";
26428
26429 let markdown_language = Arc::new(Language::new(
26430 LanguageConfig {
26431 name: "Markdown".into(),
26432 ..LanguageConfig::default()
26433 },
26434 None,
26435 ));
26436
26437 let mut cx = EditorTestContext::new(cx).await;
26438 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26439 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26440
26441 cx.update_editor(|editor, window, cx| {
26442 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26443 editor.paste(&Paste, window, cx);
26444 });
26445
26446 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26447}
26448
26449#[gpui::test]
26450async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26451 cx: &mut gpui::TestAppContext,
26452) {
26453 init_test(cx, |_| {});
26454
26455 let url = "https://zed.dev";
26456
26457 let markdown_language = Arc::new(Language::new(
26458 LanguageConfig {
26459 name: "Rust".into(),
26460 ..LanguageConfig::default()
26461 },
26462 None,
26463 ));
26464
26465 let mut cx = EditorTestContext::new(cx).await;
26466 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26467 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26468
26469 cx.update_editor(|editor, window, cx| {
26470 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26471 editor.paste(&Paste, window, cx);
26472 });
26473
26474 cx.assert_editor_state(&format!(
26475 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26476 ));
26477}
26478
26479#[gpui::test]
26480async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26481 cx: &mut TestAppContext,
26482) {
26483 init_test(cx, |_| {});
26484
26485 let url = "https://zed.dev";
26486
26487 let markdown_language = Arc::new(Language::new(
26488 LanguageConfig {
26489 name: "Markdown".into(),
26490 ..LanguageConfig::default()
26491 },
26492 None,
26493 ));
26494
26495 let (editor, cx) = cx.add_window_view(|window, cx| {
26496 let multi_buffer = MultiBuffer::build_multi(
26497 [
26498 ("this will embed -> link", vec![Point::row_range(0..1)]),
26499 ("this will replace -> link", vec![Point::row_range(0..1)]),
26500 ],
26501 cx,
26502 );
26503 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26504 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26505 s.select_ranges(vec![
26506 Point::new(0, 19)..Point::new(0, 23),
26507 Point::new(1, 21)..Point::new(1, 25),
26508 ])
26509 });
26510 let first_buffer_id = multi_buffer
26511 .read(cx)
26512 .excerpt_buffer_ids()
26513 .into_iter()
26514 .next()
26515 .unwrap();
26516 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26517 first_buffer.update(cx, |buffer, cx| {
26518 buffer.set_language(Some(markdown_language.clone()), cx);
26519 });
26520
26521 editor
26522 });
26523 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26524
26525 cx.update_editor(|editor, window, cx| {
26526 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26527 editor.paste(&Paste, window, cx);
26528 });
26529
26530 cx.assert_editor_state(&format!(
26531 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26532 ));
26533}
26534
26535#[gpui::test]
26536async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26537 init_test(cx, |_| {});
26538
26539 let fs = FakeFs::new(cx.executor());
26540 fs.insert_tree(
26541 path!("/project"),
26542 json!({
26543 "first.rs": "# First Document\nSome content here.",
26544 "second.rs": "Plain text content for second file.",
26545 }),
26546 )
26547 .await;
26548
26549 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26550 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26551 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26552
26553 let language = rust_lang();
26554 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26555 language_registry.add(language.clone());
26556 let mut fake_servers = language_registry.register_fake_lsp(
26557 "Rust",
26558 FakeLspAdapter {
26559 ..FakeLspAdapter::default()
26560 },
26561 );
26562
26563 let buffer1 = project
26564 .update(cx, |project, cx| {
26565 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26566 })
26567 .await
26568 .unwrap();
26569 let buffer2 = project
26570 .update(cx, |project, cx| {
26571 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26572 })
26573 .await
26574 .unwrap();
26575
26576 let multi_buffer = cx.new(|cx| {
26577 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26578 multi_buffer.set_excerpts_for_path(
26579 PathKey::for_buffer(&buffer1, cx),
26580 buffer1.clone(),
26581 [Point::zero()..buffer1.read(cx).max_point()],
26582 3,
26583 cx,
26584 );
26585 multi_buffer.set_excerpts_for_path(
26586 PathKey::for_buffer(&buffer2, cx),
26587 buffer2.clone(),
26588 [Point::zero()..buffer1.read(cx).max_point()],
26589 3,
26590 cx,
26591 );
26592 multi_buffer
26593 });
26594
26595 let (editor, cx) = cx.add_window_view(|window, cx| {
26596 Editor::new(
26597 EditorMode::full(),
26598 multi_buffer,
26599 Some(project.clone()),
26600 window,
26601 cx,
26602 )
26603 });
26604
26605 let fake_language_server = fake_servers.next().await.unwrap();
26606
26607 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26608
26609 let save = editor.update_in(cx, |editor, window, cx| {
26610 assert!(editor.is_dirty(cx));
26611
26612 editor.save(
26613 SaveOptions {
26614 format: true,
26615 autosave: true,
26616 },
26617 project,
26618 window,
26619 cx,
26620 )
26621 });
26622 let (start_edit_tx, start_edit_rx) = oneshot::channel();
26623 let (done_edit_tx, done_edit_rx) = oneshot::channel();
26624 let mut done_edit_rx = Some(done_edit_rx);
26625 let mut start_edit_tx = Some(start_edit_tx);
26626
26627 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
26628 start_edit_tx.take().unwrap().send(()).unwrap();
26629 let done_edit_rx = done_edit_rx.take().unwrap();
26630 async move {
26631 done_edit_rx.await.unwrap();
26632 Ok(None)
26633 }
26634 });
26635
26636 start_edit_rx.await.unwrap();
26637 buffer2
26638 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
26639 .unwrap();
26640
26641 done_edit_tx.send(()).unwrap();
26642
26643 save.await.unwrap();
26644 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
26645}
26646
26647#[track_caller]
26648fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26649 editor
26650 .all_inlays(cx)
26651 .into_iter()
26652 .filter_map(|inlay| inlay.get_color())
26653 .map(Rgba::from)
26654 .collect()
26655}
26656
26657#[gpui::test]
26658fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
26659 init_test(cx, |_| {});
26660
26661 let editor = cx.add_window(|window, cx| {
26662 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
26663 build_editor(buffer, window, cx)
26664 });
26665
26666 editor
26667 .update(cx, |editor, window, cx| {
26668 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26669 s.select_display_ranges([
26670 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
26671 ])
26672 });
26673
26674 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
26675
26676 assert_eq!(
26677 editor.display_text(cx),
26678 "line1\nline2\nline2",
26679 "Duplicating last line upward should create duplicate above, not on same line"
26680 );
26681
26682 assert_eq!(
26683 editor.selections.display_ranges(cx),
26684 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)],
26685 "Selection should remain on the original line"
26686 );
26687 })
26688 .unwrap();
26689}
26690
26691#[gpui::test]
26692async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
26693 init_test(cx, |_| {});
26694
26695 let mut cx = EditorTestContext::new(cx).await;
26696
26697 cx.set_state("line1\nline2ˇ");
26698
26699 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
26700
26701 let clipboard_text = cx
26702 .read_from_clipboard()
26703 .and_then(|item| item.text().as_deref().map(str::to_string));
26704
26705 assert_eq!(
26706 clipboard_text,
26707 Some("line2\n".to_string()),
26708 "Copying a line without trailing newline should include a newline"
26709 );
26710
26711 cx.set_state("line1\nˇ");
26712
26713 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
26714
26715 cx.assert_editor_state("line1\nline2\nˇ");
26716}