1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use collections::HashMap;
17use futures::{StreamExt, channel::oneshot};
18use gpui::{
19 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
20 VisualTestContext, WindowBounds, WindowOptions, div,
21};
22use indoc::indoc;
23use language::{
24 BracketPairConfig,
25 Capability::ReadWrite,
26 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
27 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
28 language_settings::{
29 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
30 SelectedFormatter,
31 },
32 tree_sitter_python,
33};
34use language_settings::Formatter;
35use lsp::CompletionParams;
36use multi_buffer::{IndentGuide, PathKey};
37use parking_lot::Mutex;
38use pretty_assertions::{assert_eq, assert_ne};
39use project::{
40 FakeFs,
41 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
42 project_settings::LspSettings,
43};
44use serde_json::{self, json};
45use settings::{
46 AllLanguageSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring,
47 ProjectSettingsContent,
48};
49use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
50use std::{
51 iter,
52 sync::atomic::{self, AtomicUsize},
53};
54use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
55use text::ToPoint as _;
56use unindent::Unindent;
57use util::{
58 assert_set_eq, path,
59 rel_path::rel_path,
60 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
61 uri,
62};
63use workspace::{
64 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
65 OpenOptions, ViewId,
66 invalid_buffer_view::InvalidBufferView,
67 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
68 register_project_item,
69};
70
71#[gpui::test]
72fn test_edit_events(cx: &mut TestAppContext) {
73 init_test(cx, |_| {});
74
75 let buffer = cx.new(|cx| {
76 let mut buffer = language::Buffer::local("123456", cx);
77 buffer.set_group_interval(Duration::from_secs(1));
78 buffer
79 });
80
81 let events = Rc::new(RefCell::new(Vec::new()));
82 let editor1 = cx.add_window({
83 let events = events.clone();
84 |window, cx| {
85 let entity = cx.entity();
86 cx.subscribe_in(
87 &entity,
88 window,
89 move |_, _, event: &EditorEvent, _, _| match event {
90 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
91 EditorEvent::BufferEdited => {
92 events.borrow_mut().push(("editor1", "buffer edited"))
93 }
94 _ => {}
95 },
96 )
97 .detach();
98 Editor::for_buffer(buffer.clone(), None, window, cx)
99 }
100 });
101
102 let editor2 = cx.add_window({
103 let events = events.clone();
104 |window, cx| {
105 cx.subscribe_in(
106 &cx.entity(),
107 window,
108 move |_, _, event: &EditorEvent, _, _| match event {
109 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
110 EditorEvent::BufferEdited => {
111 events.borrow_mut().push(("editor2", "buffer edited"))
112 }
113 _ => {}
114 },
115 )
116 .detach();
117 Editor::for_buffer(buffer.clone(), None, window, cx)
118 }
119 });
120
121 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
122
123 // Mutating editor 1 will emit an `Edited` event only for that editor.
124 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
125 assert_eq!(
126 mem::take(&mut *events.borrow_mut()),
127 [
128 ("editor1", "edited"),
129 ("editor1", "buffer edited"),
130 ("editor2", "buffer edited"),
131 ]
132 );
133
134 // Mutating editor 2 will emit an `Edited` event only for that editor.
135 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
136 assert_eq!(
137 mem::take(&mut *events.borrow_mut()),
138 [
139 ("editor2", "edited"),
140 ("editor1", "buffer edited"),
141 ("editor2", "buffer edited"),
142 ]
143 );
144
145 // Undoing on editor 1 will emit an `Edited` event only for that editor.
146 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
147 assert_eq!(
148 mem::take(&mut *events.borrow_mut()),
149 [
150 ("editor1", "edited"),
151 ("editor1", "buffer edited"),
152 ("editor2", "buffer edited"),
153 ]
154 );
155
156 // Redoing on editor 1 will emit an `Edited` event only for that editor.
157 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
158 assert_eq!(
159 mem::take(&mut *events.borrow_mut()),
160 [
161 ("editor1", "edited"),
162 ("editor1", "buffer edited"),
163 ("editor2", "buffer edited"),
164 ]
165 );
166
167 // Undoing on editor 2 will emit an `Edited` event only for that editor.
168 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
169 assert_eq!(
170 mem::take(&mut *events.borrow_mut()),
171 [
172 ("editor2", "edited"),
173 ("editor1", "buffer edited"),
174 ("editor2", "buffer edited"),
175 ]
176 );
177
178 // Redoing on editor 2 will emit an `Edited` event only for that editor.
179 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
180 assert_eq!(
181 mem::take(&mut *events.borrow_mut()),
182 [
183 ("editor2", "edited"),
184 ("editor1", "buffer edited"),
185 ("editor2", "buffer edited"),
186 ]
187 );
188
189 // No event is emitted when the mutation is a no-op.
190 _ = editor2.update(cx, |editor, window, cx| {
191 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
192 s.select_ranges([0..0])
193 });
194
195 editor.backspace(&Backspace, window, cx);
196 });
197 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
198}
199
200#[gpui::test]
201fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
202 init_test(cx, |_| {});
203
204 let mut now = Instant::now();
205 let group_interval = Duration::from_millis(1);
206 let buffer = cx.new(|cx| {
207 let mut buf = language::Buffer::local("123456", cx);
208 buf.set_group_interval(group_interval);
209 buf
210 });
211 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
212 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
213
214 _ = editor.update(cx, |editor, window, cx| {
215 editor.start_transaction_at(now, window, cx);
216 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
217 s.select_ranges([2..4])
218 });
219
220 editor.insert("cd", window, cx);
221 editor.end_transaction_at(now, cx);
222 assert_eq!(editor.text(cx), "12cd56");
223 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
224
225 editor.start_transaction_at(now, window, cx);
226 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
227 s.select_ranges([4..5])
228 });
229 editor.insert("e", window, cx);
230 editor.end_transaction_at(now, cx);
231 assert_eq!(editor.text(cx), "12cde6");
232 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
233
234 now += group_interval + Duration::from_millis(1);
235 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
236 s.select_ranges([2..2])
237 });
238
239 // Simulate an edit in another editor
240 buffer.update(cx, |buffer, cx| {
241 buffer.start_transaction_at(now, cx);
242 buffer.edit([(0..1, "a")], None, cx);
243 buffer.edit([(1..1, "b")], None, cx);
244 buffer.end_transaction_at(now, cx);
245 });
246
247 assert_eq!(editor.text(cx), "ab2cde6");
248 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
249
250 // Last transaction happened past the group interval in a different editor.
251 // Undo it individually and don't restore selections.
252 editor.undo(&Undo, window, cx);
253 assert_eq!(editor.text(cx), "12cde6");
254 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
255
256 // First two transactions happened within the group interval in this editor.
257 // Undo them together and restore selections.
258 editor.undo(&Undo, window, cx);
259 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
260 assert_eq!(editor.text(cx), "123456");
261 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
262
263 // Redo the first two transactions together.
264 editor.redo(&Redo, window, cx);
265 assert_eq!(editor.text(cx), "12cde6");
266 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
267
268 // Redo the last transaction on its own.
269 editor.redo(&Redo, window, cx);
270 assert_eq!(editor.text(cx), "ab2cde6");
271 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
272
273 // Test empty transactions.
274 editor.start_transaction_at(now, window, cx);
275 editor.end_transaction_at(now, cx);
276 editor.undo(&Undo, window, cx);
277 assert_eq!(editor.text(cx), "12cde6");
278 });
279}
280
281#[gpui::test]
282fn test_ime_composition(cx: &mut TestAppContext) {
283 init_test(cx, |_| {});
284
285 let buffer = cx.new(|cx| {
286 let mut buffer = language::Buffer::local("abcde", cx);
287 // Ensure automatic grouping doesn't occur.
288 buffer.set_group_interval(Duration::ZERO);
289 buffer
290 });
291
292 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
293 cx.add_window(|window, cx| {
294 let mut editor = build_editor(buffer.clone(), window, cx);
295
296 // Start a new IME composition.
297 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
298 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
299 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
300 assert_eq!(editor.text(cx), "äbcde");
301 assert_eq!(
302 editor.marked_text_ranges(cx),
303 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
304 );
305
306 // Finalize IME composition.
307 editor.replace_text_in_range(None, "ā", window, cx);
308 assert_eq!(editor.text(cx), "ābcde");
309 assert_eq!(editor.marked_text_ranges(cx), None);
310
311 // IME composition edits are grouped and are undone/redone at once.
312 editor.undo(&Default::default(), window, cx);
313 assert_eq!(editor.text(cx), "abcde");
314 assert_eq!(editor.marked_text_ranges(cx), None);
315 editor.redo(&Default::default(), window, cx);
316 assert_eq!(editor.text(cx), "ābcde");
317 assert_eq!(editor.marked_text_ranges(cx), None);
318
319 // Start a new IME composition.
320 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
321 assert_eq!(
322 editor.marked_text_ranges(cx),
323 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
324 );
325
326 // Undoing during an IME composition cancels it.
327 editor.undo(&Default::default(), window, cx);
328 assert_eq!(editor.text(cx), "ābcde");
329 assert_eq!(editor.marked_text_ranges(cx), None);
330
331 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
332 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
333 assert_eq!(editor.text(cx), "ābcdè");
334 assert_eq!(
335 editor.marked_text_ranges(cx),
336 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
337 );
338
339 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
340 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
341 assert_eq!(editor.text(cx), "ābcdę");
342 assert_eq!(editor.marked_text_ranges(cx), None);
343
344 // Start a new IME composition with multiple cursors.
345 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
346 s.select_ranges([
347 OffsetUtf16(1)..OffsetUtf16(1),
348 OffsetUtf16(3)..OffsetUtf16(3),
349 OffsetUtf16(5)..OffsetUtf16(5),
350 ])
351 });
352 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
353 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
354 assert_eq!(
355 editor.marked_text_ranges(cx),
356 Some(vec![
357 OffsetUtf16(0)..OffsetUtf16(3),
358 OffsetUtf16(4)..OffsetUtf16(7),
359 OffsetUtf16(8)..OffsetUtf16(11)
360 ])
361 );
362
363 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
364 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
365 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
366 assert_eq!(
367 editor.marked_text_ranges(cx),
368 Some(vec![
369 OffsetUtf16(1)..OffsetUtf16(2),
370 OffsetUtf16(5)..OffsetUtf16(6),
371 OffsetUtf16(9)..OffsetUtf16(10)
372 ])
373 );
374
375 // Finalize IME composition with multiple cursors.
376 editor.replace_text_in_range(Some(9..10), "2", window, cx);
377 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
378 assert_eq!(editor.marked_text_ranges(cx), None);
379
380 editor
381 });
382}
383
384#[gpui::test]
385fn test_selection_with_mouse(cx: &mut TestAppContext) {
386 init_test(cx, |_| {});
387
388 let editor = cx.add_window(|window, cx| {
389 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
390 build_editor(buffer, window, cx)
391 });
392
393 _ = editor.update(cx, |editor, window, cx| {
394 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
395 });
396 assert_eq!(
397 editor
398 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
399 .unwrap(),
400 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
401 );
402
403 _ = editor.update(cx, |editor, window, cx| {
404 editor.update_selection(
405 DisplayPoint::new(DisplayRow(3), 3),
406 0,
407 gpui::Point::<f32>::default(),
408 window,
409 cx,
410 );
411 });
412
413 assert_eq!(
414 editor
415 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
416 .unwrap(),
417 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
418 );
419
420 _ = editor.update(cx, |editor, window, cx| {
421 editor.update_selection(
422 DisplayPoint::new(DisplayRow(1), 1),
423 0,
424 gpui::Point::<f32>::default(),
425 window,
426 cx,
427 );
428 });
429
430 assert_eq!(
431 editor
432 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
433 .unwrap(),
434 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
435 );
436
437 _ = editor.update(cx, |editor, window, cx| {
438 editor.end_selection(window, cx);
439 editor.update_selection(
440 DisplayPoint::new(DisplayRow(3), 3),
441 0,
442 gpui::Point::<f32>::default(),
443 window,
444 cx,
445 );
446 });
447
448 assert_eq!(
449 editor
450 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
451 .unwrap(),
452 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
453 );
454
455 _ = editor.update(cx, |editor, window, cx| {
456 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
457 editor.update_selection(
458 DisplayPoint::new(DisplayRow(0), 0),
459 0,
460 gpui::Point::<f32>::default(),
461 window,
462 cx,
463 );
464 });
465
466 assert_eq!(
467 editor
468 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
469 .unwrap(),
470 [
471 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
472 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
473 ]
474 );
475
476 _ = editor.update(cx, |editor, window, cx| {
477 editor.end_selection(window, cx);
478 });
479
480 assert_eq!(
481 editor
482 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
483 .unwrap(),
484 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
485 );
486}
487
488#[gpui::test]
489fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
490 init_test(cx, |_| {});
491
492 let editor = cx.add_window(|window, cx| {
493 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
494 build_editor(buffer, window, cx)
495 });
496
497 _ = editor.update(cx, |editor, window, cx| {
498 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
499 });
500
501 _ = editor.update(cx, |editor, window, cx| {
502 editor.end_selection(window, cx);
503 });
504
505 _ = editor.update(cx, |editor, window, cx| {
506 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
507 });
508
509 _ = editor.update(cx, |editor, window, cx| {
510 editor.end_selection(window, cx);
511 });
512
513 assert_eq!(
514 editor
515 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
516 .unwrap(),
517 [
518 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
519 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
520 ]
521 );
522
523 _ = editor.update(cx, |editor, window, cx| {
524 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
525 });
526
527 _ = editor.update(cx, |editor, window, cx| {
528 editor.end_selection(window, cx);
529 });
530
531 assert_eq!(
532 editor
533 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
534 .unwrap(),
535 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
536 );
537}
538
539#[gpui::test]
540fn test_canceling_pending_selection(cx: &mut TestAppContext) {
541 init_test(cx, |_| {});
542
543 let editor = cx.add_window(|window, cx| {
544 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
545 build_editor(buffer, window, cx)
546 });
547
548 _ = editor.update(cx, |editor, window, cx| {
549 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
550 assert_eq!(
551 editor.selections.display_ranges(cx),
552 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
553 );
554 });
555
556 _ = editor.update(cx, |editor, window, cx| {
557 editor.update_selection(
558 DisplayPoint::new(DisplayRow(3), 3),
559 0,
560 gpui::Point::<f32>::default(),
561 window,
562 cx,
563 );
564 assert_eq!(
565 editor.selections.display_ranges(cx),
566 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
567 );
568 });
569
570 _ = editor.update(cx, |editor, window, cx| {
571 editor.cancel(&Cancel, window, cx);
572 editor.update_selection(
573 DisplayPoint::new(DisplayRow(1), 1),
574 0,
575 gpui::Point::<f32>::default(),
576 window,
577 cx,
578 );
579 assert_eq!(
580 editor.selections.display_ranges(cx),
581 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
582 );
583 });
584}
585
586#[gpui::test]
587fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
588 init_test(cx, |_| {});
589
590 let editor = cx.add_window(|window, cx| {
591 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
592 build_editor(buffer, window, cx)
593 });
594
595 _ = editor.update(cx, |editor, window, cx| {
596 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
597 assert_eq!(
598 editor.selections.display_ranges(cx),
599 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
600 );
601
602 editor.move_down(&Default::default(), window, cx);
603 assert_eq!(
604 editor.selections.display_ranges(cx),
605 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
606 );
607
608 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
609 assert_eq!(
610 editor.selections.display_ranges(cx),
611 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
612 );
613
614 editor.move_up(&Default::default(), window, cx);
615 assert_eq!(
616 editor.selections.display_ranges(cx),
617 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
618 );
619 });
620}
621
622#[gpui::test]
623fn test_extending_selection(cx: &mut TestAppContext) {
624 init_test(cx, |_| {});
625
626 let editor = cx.add_window(|window, cx| {
627 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
628 build_editor(buffer, window, cx)
629 });
630
631 _ = editor.update(cx, |editor, window, cx| {
632 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
633 editor.end_selection(window, cx);
634 assert_eq!(
635 editor.selections.display_ranges(cx),
636 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
637 );
638
639 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
640 editor.end_selection(window, cx);
641 assert_eq!(
642 editor.selections.display_ranges(cx),
643 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
644 );
645
646 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
647 editor.end_selection(window, cx);
648 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
649 assert_eq!(
650 editor.selections.display_ranges(cx),
651 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
652 );
653
654 editor.update_selection(
655 DisplayPoint::new(DisplayRow(0), 1),
656 0,
657 gpui::Point::<f32>::default(),
658 window,
659 cx,
660 );
661 editor.end_selection(window, cx);
662 assert_eq!(
663 editor.selections.display_ranges(cx),
664 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
665 );
666
667 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
668 editor.end_selection(window, cx);
669 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
670 editor.end_selection(window, cx);
671 assert_eq!(
672 editor.selections.display_ranges(cx),
673 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
674 );
675
676 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
677 assert_eq!(
678 editor.selections.display_ranges(cx),
679 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
680 );
681
682 editor.update_selection(
683 DisplayPoint::new(DisplayRow(0), 6),
684 0,
685 gpui::Point::<f32>::default(),
686 window,
687 cx,
688 );
689 assert_eq!(
690 editor.selections.display_ranges(cx),
691 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
692 );
693
694 editor.update_selection(
695 DisplayPoint::new(DisplayRow(0), 1),
696 0,
697 gpui::Point::<f32>::default(),
698 window,
699 cx,
700 );
701 editor.end_selection(window, cx);
702 assert_eq!(
703 editor.selections.display_ranges(cx),
704 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
705 );
706 });
707}
708
709#[gpui::test]
710fn test_clone(cx: &mut TestAppContext) {
711 init_test(cx, |_| {});
712
713 let (text, selection_ranges) = marked_text_ranges(
714 indoc! {"
715 one
716 two
717 threeˇ
718 four
719 fiveˇ
720 "},
721 true,
722 );
723
724 let editor = cx.add_window(|window, cx| {
725 let buffer = MultiBuffer::build_simple(&text, cx);
726 build_editor(buffer, window, cx)
727 });
728
729 _ = editor.update(cx, |editor, window, cx| {
730 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
731 s.select_ranges(selection_ranges.clone())
732 });
733 editor.fold_creases(
734 vec![
735 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
736 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
737 ],
738 true,
739 window,
740 cx,
741 );
742 });
743
744 let cloned_editor = editor
745 .update(cx, |editor, _, cx| {
746 cx.open_window(Default::default(), |window, cx| {
747 cx.new(|cx| editor.clone(window, cx))
748 })
749 })
750 .unwrap()
751 .unwrap();
752
753 let snapshot = editor
754 .update(cx, |e, window, cx| e.snapshot(window, cx))
755 .unwrap();
756 let cloned_snapshot = cloned_editor
757 .update(cx, |e, window, cx| e.snapshot(window, cx))
758 .unwrap();
759
760 assert_eq!(
761 cloned_editor
762 .update(cx, |e, _, cx| e.display_text(cx))
763 .unwrap(),
764 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
765 );
766 assert_eq!(
767 cloned_snapshot
768 .folds_in_range(0..text.len())
769 .collect::<Vec<_>>(),
770 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
771 );
772 assert_set_eq!(
773 cloned_editor
774 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
775 .unwrap(),
776 editor
777 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
778 .unwrap()
779 );
780 assert_set_eq!(
781 cloned_editor
782 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
783 .unwrap(),
784 editor
785 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
786 .unwrap()
787 );
788}
789
790#[gpui::test]
791async fn test_navigation_history(cx: &mut TestAppContext) {
792 init_test(cx, |_| {});
793
794 use workspace::item::Item;
795
796 let fs = FakeFs::new(cx.executor());
797 let project = Project::test(fs, [], cx).await;
798 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
799 let pane = workspace
800 .update(cx, |workspace, _, _| workspace.active_pane().clone())
801 .unwrap();
802
803 _ = workspace.update(cx, |_v, window, cx| {
804 cx.new(|cx| {
805 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
806 let mut editor = build_editor(buffer, window, cx);
807 let handle = cx.entity();
808 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
809
810 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
811 editor.nav_history.as_mut().unwrap().pop_backward(cx)
812 }
813
814 // Move the cursor a small distance.
815 // Nothing is added to the navigation history.
816 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
817 s.select_display_ranges([
818 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
819 ])
820 });
821 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
822 s.select_display_ranges([
823 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
824 ])
825 });
826 assert!(pop_history(&mut editor, cx).is_none());
827
828 // Move the cursor a large distance.
829 // The history can jump back to the previous position.
830 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
831 s.select_display_ranges([
832 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
833 ])
834 });
835 let nav_entry = pop_history(&mut editor, cx).unwrap();
836 editor.navigate(nav_entry.data.unwrap(), window, cx);
837 assert_eq!(nav_entry.item.id(), cx.entity_id());
838 assert_eq!(
839 editor.selections.display_ranges(cx),
840 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
841 );
842 assert!(pop_history(&mut editor, cx).is_none());
843
844 // Move the cursor a small distance via the mouse.
845 // Nothing is added to the navigation history.
846 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
847 editor.end_selection(window, cx);
848 assert_eq!(
849 editor.selections.display_ranges(cx),
850 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
851 );
852 assert!(pop_history(&mut editor, cx).is_none());
853
854 // Move the cursor a large distance via the mouse.
855 // The history can jump back to the previous position.
856 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
857 editor.end_selection(window, cx);
858 assert_eq!(
859 editor.selections.display_ranges(cx),
860 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
861 );
862 let nav_entry = pop_history(&mut editor, cx).unwrap();
863 editor.navigate(nav_entry.data.unwrap(), window, cx);
864 assert_eq!(nav_entry.item.id(), cx.entity_id());
865 assert_eq!(
866 editor.selections.display_ranges(cx),
867 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
868 );
869 assert!(pop_history(&mut editor, cx).is_none());
870
871 // Set scroll position to check later
872 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
873 let original_scroll_position = editor.scroll_manager.anchor();
874
875 // Jump to the end of the document and adjust scroll
876 editor.move_to_end(&MoveToEnd, window, cx);
877 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
878 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
879
880 let nav_entry = pop_history(&mut editor, cx).unwrap();
881 editor.navigate(nav_entry.data.unwrap(), window, cx);
882 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
883
884 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
885 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
886 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
887 let invalid_point = Point::new(9999, 0);
888 editor.navigate(
889 Box::new(NavigationData {
890 cursor_anchor: invalid_anchor,
891 cursor_position: invalid_point,
892 scroll_anchor: ScrollAnchor {
893 anchor: invalid_anchor,
894 offset: Default::default(),
895 },
896 scroll_top_row: invalid_point.row,
897 }),
898 window,
899 cx,
900 );
901 assert_eq!(
902 editor.selections.display_ranges(cx),
903 &[editor.max_point(cx)..editor.max_point(cx)]
904 );
905 assert_eq!(
906 editor.scroll_position(cx),
907 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
908 );
909
910 editor
911 })
912 });
913}
914
915#[gpui::test]
916fn test_cancel(cx: &mut TestAppContext) {
917 init_test(cx, |_| {});
918
919 let editor = cx.add_window(|window, cx| {
920 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
921 build_editor(buffer, window, cx)
922 });
923
924 _ = editor.update(cx, |editor, window, cx| {
925 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
926 editor.update_selection(
927 DisplayPoint::new(DisplayRow(1), 1),
928 0,
929 gpui::Point::<f32>::default(),
930 window,
931 cx,
932 );
933 editor.end_selection(window, cx);
934
935 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
936 editor.update_selection(
937 DisplayPoint::new(DisplayRow(0), 3),
938 0,
939 gpui::Point::<f32>::default(),
940 window,
941 cx,
942 );
943 editor.end_selection(window, cx);
944 assert_eq!(
945 editor.selections.display_ranges(cx),
946 [
947 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
948 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
949 ]
950 );
951 });
952
953 _ = editor.update(cx, |editor, window, cx| {
954 editor.cancel(&Cancel, window, cx);
955 assert_eq!(
956 editor.selections.display_ranges(cx),
957 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
958 );
959 });
960
961 _ = editor.update(cx, |editor, window, cx| {
962 editor.cancel(&Cancel, window, cx);
963 assert_eq!(
964 editor.selections.display_ranges(cx),
965 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
966 );
967 });
968}
969
970#[gpui::test]
971fn test_fold_action(cx: &mut TestAppContext) {
972 init_test(cx, |_| {});
973
974 let editor = cx.add_window(|window, cx| {
975 let buffer = MultiBuffer::build_simple(
976 &"
977 impl Foo {
978 // Hello!
979
980 fn a() {
981 1
982 }
983
984 fn b() {
985 2
986 }
987
988 fn c() {
989 3
990 }
991 }
992 "
993 .unindent(),
994 cx,
995 );
996 build_editor(buffer, window, cx)
997 });
998
999 _ = editor.update(cx, |editor, window, cx| {
1000 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1001 s.select_display_ranges([
1002 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1003 ]);
1004 });
1005 editor.fold(&Fold, window, cx);
1006 assert_eq!(
1007 editor.display_text(cx),
1008 "
1009 impl Foo {
1010 // Hello!
1011
1012 fn a() {
1013 1
1014 }
1015
1016 fn b() {⋯
1017 }
1018
1019 fn c() {⋯
1020 }
1021 }
1022 "
1023 .unindent(),
1024 );
1025
1026 editor.fold(&Fold, window, cx);
1027 assert_eq!(
1028 editor.display_text(cx),
1029 "
1030 impl Foo {⋯
1031 }
1032 "
1033 .unindent(),
1034 );
1035
1036 editor.unfold_lines(&UnfoldLines, window, cx);
1037 assert_eq!(
1038 editor.display_text(cx),
1039 "
1040 impl Foo {
1041 // Hello!
1042
1043 fn a() {
1044 1
1045 }
1046
1047 fn b() {⋯
1048 }
1049
1050 fn c() {⋯
1051 }
1052 }
1053 "
1054 .unindent(),
1055 );
1056
1057 editor.unfold_lines(&UnfoldLines, window, cx);
1058 assert_eq!(
1059 editor.display_text(cx),
1060 editor.buffer.read(cx).read(cx).text()
1061 );
1062 });
1063}
1064
1065#[gpui::test]
1066fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1067 init_test(cx, |_| {});
1068
1069 let editor = cx.add_window(|window, cx| {
1070 let buffer = MultiBuffer::build_simple(
1071 &"
1072 class Foo:
1073 # Hello!
1074
1075 def a():
1076 print(1)
1077
1078 def b():
1079 print(2)
1080
1081 def c():
1082 print(3)
1083 "
1084 .unindent(),
1085 cx,
1086 );
1087 build_editor(buffer, window, cx)
1088 });
1089
1090 _ = editor.update(cx, |editor, window, cx| {
1091 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1092 s.select_display_ranges([
1093 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1094 ]);
1095 });
1096 editor.fold(&Fold, window, cx);
1097 assert_eq!(
1098 editor.display_text(cx),
1099 "
1100 class Foo:
1101 # Hello!
1102
1103 def a():
1104 print(1)
1105
1106 def b():⋯
1107
1108 def c():⋯
1109 "
1110 .unindent(),
1111 );
1112
1113 editor.fold(&Fold, window, cx);
1114 assert_eq!(
1115 editor.display_text(cx),
1116 "
1117 class Foo:⋯
1118 "
1119 .unindent(),
1120 );
1121
1122 editor.unfold_lines(&UnfoldLines, window, cx);
1123 assert_eq!(
1124 editor.display_text(cx),
1125 "
1126 class Foo:
1127 # Hello!
1128
1129 def a():
1130 print(1)
1131
1132 def b():⋯
1133
1134 def c():⋯
1135 "
1136 .unindent(),
1137 );
1138
1139 editor.unfold_lines(&UnfoldLines, window, cx);
1140 assert_eq!(
1141 editor.display_text(cx),
1142 editor.buffer.read(cx).read(cx).text()
1143 );
1144 });
1145}
1146
1147#[gpui::test]
1148fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1149 init_test(cx, |_| {});
1150
1151 let editor = cx.add_window(|window, cx| {
1152 let buffer = MultiBuffer::build_simple(
1153 &"
1154 class Foo:
1155 # Hello!
1156
1157 def a():
1158 print(1)
1159
1160 def b():
1161 print(2)
1162
1163
1164 def c():
1165 print(3)
1166
1167
1168 "
1169 .unindent(),
1170 cx,
1171 );
1172 build_editor(buffer, window, cx)
1173 });
1174
1175 _ = editor.update(cx, |editor, window, cx| {
1176 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1177 s.select_display_ranges([
1178 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1179 ]);
1180 });
1181 editor.fold(&Fold, window, cx);
1182 assert_eq!(
1183 editor.display_text(cx),
1184 "
1185 class Foo:
1186 # Hello!
1187
1188 def a():
1189 print(1)
1190
1191 def b():⋯
1192
1193
1194 def c():⋯
1195
1196
1197 "
1198 .unindent(),
1199 );
1200
1201 editor.fold(&Fold, window, cx);
1202 assert_eq!(
1203 editor.display_text(cx),
1204 "
1205 class Foo:⋯
1206
1207
1208 "
1209 .unindent(),
1210 );
1211
1212 editor.unfold_lines(&UnfoldLines, window, cx);
1213 assert_eq!(
1214 editor.display_text(cx),
1215 "
1216 class Foo:
1217 # Hello!
1218
1219 def a():
1220 print(1)
1221
1222 def b():⋯
1223
1224
1225 def c():⋯
1226
1227
1228 "
1229 .unindent(),
1230 );
1231
1232 editor.unfold_lines(&UnfoldLines, window, cx);
1233 assert_eq!(
1234 editor.display_text(cx),
1235 editor.buffer.read(cx).read(cx).text()
1236 );
1237 });
1238}
1239
1240#[gpui::test]
1241fn test_fold_at_level(cx: &mut TestAppContext) {
1242 init_test(cx, |_| {});
1243
1244 let editor = cx.add_window(|window, cx| {
1245 let buffer = MultiBuffer::build_simple(
1246 &"
1247 class Foo:
1248 # Hello!
1249
1250 def a():
1251 print(1)
1252
1253 def b():
1254 print(2)
1255
1256
1257 class Bar:
1258 # World!
1259
1260 def a():
1261 print(1)
1262
1263 def b():
1264 print(2)
1265
1266
1267 "
1268 .unindent(),
1269 cx,
1270 );
1271 build_editor(buffer, window, cx)
1272 });
1273
1274 _ = editor.update(cx, |editor, window, cx| {
1275 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1276 assert_eq!(
1277 editor.display_text(cx),
1278 "
1279 class Foo:
1280 # Hello!
1281
1282 def a():⋯
1283
1284 def b():⋯
1285
1286
1287 class Bar:
1288 # World!
1289
1290 def a():⋯
1291
1292 def b():⋯
1293
1294
1295 "
1296 .unindent(),
1297 );
1298
1299 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1300 assert_eq!(
1301 editor.display_text(cx),
1302 "
1303 class Foo:⋯
1304
1305
1306 class Bar:⋯
1307
1308
1309 "
1310 .unindent(),
1311 );
1312
1313 editor.unfold_all(&UnfoldAll, window, cx);
1314 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1315 assert_eq!(
1316 editor.display_text(cx),
1317 "
1318 class Foo:
1319 # Hello!
1320
1321 def a():
1322 print(1)
1323
1324 def b():
1325 print(2)
1326
1327
1328 class Bar:
1329 # World!
1330
1331 def a():
1332 print(1)
1333
1334 def b():
1335 print(2)
1336
1337
1338 "
1339 .unindent(),
1340 );
1341
1342 assert_eq!(
1343 editor.display_text(cx),
1344 editor.buffer.read(cx).read(cx).text()
1345 );
1346 let (_, positions) = marked_text_ranges(
1347 &"
1348 class Foo:
1349 # Hello!
1350
1351 def a():
1352 print(1)
1353
1354 def b():
1355 p«riˇ»nt(2)
1356
1357
1358 class Bar:
1359 # World!
1360
1361 def a():
1362 «ˇprint(1)
1363
1364 def b():
1365 print(2)»
1366
1367
1368 "
1369 .unindent(),
1370 true,
1371 );
1372
1373 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1374 s.select_ranges(positions)
1375 });
1376
1377 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1378 assert_eq!(
1379 editor.display_text(cx),
1380 "
1381 class Foo:
1382 # Hello!
1383
1384 def a():⋯
1385
1386 def b():
1387 print(2)
1388
1389
1390 class Bar:
1391 # World!
1392
1393 def a():
1394 print(1)
1395
1396 def b():
1397 print(2)
1398
1399
1400 "
1401 .unindent(),
1402 );
1403 });
1404}
1405
1406#[gpui::test]
1407fn test_move_cursor(cx: &mut TestAppContext) {
1408 init_test(cx, |_| {});
1409
1410 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1411 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1412
1413 buffer.update(cx, |buffer, cx| {
1414 buffer.edit(
1415 vec![
1416 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1417 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1418 ],
1419 None,
1420 cx,
1421 );
1422 });
1423 _ = editor.update(cx, |editor, window, cx| {
1424 assert_eq!(
1425 editor.selections.display_ranges(cx),
1426 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1427 );
1428
1429 editor.move_down(&MoveDown, window, cx);
1430 assert_eq!(
1431 editor.selections.display_ranges(cx),
1432 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1433 );
1434
1435 editor.move_right(&MoveRight, window, cx);
1436 assert_eq!(
1437 editor.selections.display_ranges(cx),
1438 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1439 );
1440
1441 editor.move_left(&MoveLeft, window, cx);
1442 assert_eq!(
1443 editor.selections.display_ranges(cx),
1444 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1445 );
1446
1447 editor.move_up(&MoveUp, window, cx);
1448 assert_eq!(
1449 editor.selections.display_ranges(cx),
1450 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1451 );
1452
1453 editor.move_to_end(&MoveToEnd, window, cx);
1454 assert_eq!(
1455 editor.selections.display_ranges(cx),
1456 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1457 );
1458
1459 editor.move_to_beginning(&MoveToBeginning, window, cx);
1460 assert_eq!(
1461 editor.selections.display_ranges(cx),
1462 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1463 );
1464
1465 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1466 s.select_display_ranges([
1467 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1468 ]);
1469 });
1470 editor.select_to_beginning(&SelectToBeginning, window, cx);
1471 assert_eq!(
1472 editor.selections.display_ranges(cx),
1473 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1474 );
1475
1476 editor.select_to_end(&SelectToEnd, window, cx);
1477 assert_eq!(
1478 editor.selections.display_ranges(cx),
1479 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1480 );
1481 });
1482}
1483
1484#[gpui::test]
1485fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1486 init_test(cx, |_| {});
1487
1488 let editor = cx.add_window(|window, cx| {
1489 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1490 build_editor(buffer, window, cx)
1491 });
1492
1493 assert_eq!('🟥'.len_utf8(), 4);
1494 assert_eq!('α'.len_utf8(), 2);
1495
1496 _ = editor.update(cx, |editor, window, cx| {
1497 editor.fold_creases(
1498 vec![
1499 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1500 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1501 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1502 ],
1503 true,
1504 window,
1505 cx,
1506 );
1507 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1508
1509 editor.move_right(&MoveRight, window, cx);
1510 assert_eq!(
1511 editor.selections.display_ranges(cx),
1512 &[empty_range(0, "🟥".len())]
1513 );
1514 editor.move_right(&MoveRight, window, cx);
1515 assert_eq!(
1516 editor.selections.display_ranges(cx),
1517 &[empty_range(0, "🟥🟧".len())]
1518 );
1519 editor.move_right(&MoveRight, window, cx);
1520 assert_eq!(
1521 editor.selections.display_ranges(cx),
1522 &[empty_range(0, "🟥🟧⋯".len())]
1523 );
1524
1525 editor.move_down(&MoveDown, window, cx);
1526 assert_eq!(
1527 editor.selections.display_ranges(cx),
1528 &[empty_range(1, "ab⋯e".len())]
1529 );
1530 editor.move_left(&MoveLeft, window, cx);
1531 assert_eq!(
1532 editor.selections.display_ranges(cx),
1533 &[empty_range(1, "ab⋯".len())]
1534 );
1535 editor.move_left(&MoveLeft, window, cx);
1536 assert_eq!(
1537 editor.selections.display_ranges(cx),
1538 &[empty_range(1, "ab".len())]
1539 );
1540 editor.move_left(&MoveLeft, window, cx);
1541 assert_eq!(
1542 editor.selections.display_ranges(cx),
1543 &[empty_range(1, "a".len())]
1544 );
1545
1546 editor.move_down(&MoveDown, window, cx);
1547 assert_eq!(
1548 editor.selections.display_ranges(cx),
1549 &[empty_range(2, "α".len())]
1550 );
1551 editor.move_right(&MoveRight, window, cx);
1552 assert_eq!(
1553 editor.selections.display_ranges(cx),
1554 &[empty_range(2, "αβ".len())]
1555 );
1556 editor.move_right(&MoveRight, window, cx);
1557 assert_eq!(
1558 editor.selections.display_ranges(cx),
1559 &[empty_range(2, "αβ⋯".len())]
1560 );
1561 editor.move_right(&MoveRight, window, cx);
1562 assert_eq!(
1563 editor.selections.display_ranges(cx),
1564 &[empty_range(2, "αβ⋯ε".len())]
1565 );
1566
1567 editor.move_up(&MoveUp, window, cx);
1568 assert_eq!(
1569 editor.selections.display_ranges(cx),
1570 &[empty_range(1, "ab⋯e".len())]
1571 );
1572 editor.move_down(&MoveDown, window, cx);
1573 assert_eq!(
1574 editor.selections.display_ranges(cx),
1575 &[empty_range(2, "αβ⋯ε".len())]
1576 );
1577 editor.move_up(&MoveUp, window, cx);
1578 assert_eq!(
1579 editor.selections.display_ranges(cx),
1580 &[empty_range(1, "ab⋯e".len())]
1581 );
1582
1583 editor.move_up(&MoveUp, window, cx);
1584 assert_eq!(
1585 editor.selections.display_ranges(cx),
1586 &[empty_range(0, "🟥🟧".len())]
1587 );
1588 editor.move_left(&MoveLeft, window, cx);
1589 assert_eq!(
1590 editor.selections.display_ranges(cx),
1591 &[empty_range(0, "🟥".len())]
1592 );
1593 editor.move_left(&MoveLeft, window, cx);
1594 assert_eq!(
1595 editor.selections.display_ranges(cx),
1596 &[empty_range(0, "".len())]
1597 );
1598 });
1599}
1600
1601#[gpui::test]
1602fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1603 init_test(cx, |_| {});
1604
1605 let editor = cx.add_window(|window, cx| {
1606 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1607 build_editor(buffer, window, cx)
1608 });
1609 _ = editor.update(cx, |editor, window, cx| {
1610 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1611 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1612 });
1613
1614 // moving above start of document should move selection to start of document,
1615 // but the next move down should still be at the original goal_x
1616 editor.move_up(&MoveUp, window, cx);
1617 assert_eq!(
1618 editor.selections.display_ranges(cx),
1619 &[empty_range(0, "".len())]
1620 );
1621
1622 editor.move_down(&MoveDown, window, cx);
1623 assert_eq!(
1624 editor.selections.display_ranges(cx),
1625 &[empty_range(1, "abcd".len())]
1626 );
1627
1628 editor.move_down(&MoveDown, window, cx);
1629 assert_eq!(
1630 editor.selections.display_ranges(cx),
1631 &[empty_range(2, "αβγ".len())]
1632 );
1633
1634 editor.move_down(&MoveDown, window, cx);
1635 assert_eq!(
1636 editor.selections.display_ranges(cx),
1637 &[empty_range(3, "abcd".len())]
1638 );
1639
1640 editor.move_down(&MoveDown, window, cx);
1641 assert_eq!(
1642 editor.selections.display_ranges(cx),
1643 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1644 );
1645
1646 // moving past end of document should not change goal_x
1647 editor.move_down(&MoveDown, window, cx);
1648 assert_eq!(
1649 editor.selections.display_ranges(cx),
1650 &[empty_range(5, "".len())]
1651 );
1652
1653 editor.move_down(&MoveDown, window, cx);
1654 assert_eq!(
1655 editor.selections.display_ranges(cx),
1656 &[empty_range(5, "".len())]
1657 );
1658
1659 editor.move_up(&MoveUp, window, cx);
1660 assert_eq!(
1661 editor.selections.display_ranges(cx),
1662 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1663 );
1664
1665 editor.move_up(&MoveUp, window, cx);
1666 assert_eq!(
1667 editor.selections.display_ranges(cx),
1668 &[empty_range(3, "abcd".len())]
1669 );
1670
1671 editor.move_up(&MoveUp, window, cx);
1672 assert_eq!(
1673 editor.selections.display_ranges(cx),
1674 &[empty_range(2, "αβγ".len())]
1675 );
1676 });
1677}
1678
1679#[gpui::test]
1680fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1681 init_test(cx, |_| {});
1682 let move_to_beg = MoveToBeginningOfLine {
1683 stop_at_soft_wraps: true,
1684 stop_at_indent: true,
1685 };
1686
1687 let delete_to_beg = DeleteToBeginningOfLine {
1688 stop_at_indent: false,
1689 };
1690
1691 let move_to_end = MoveToEndOfLine {
1692 stop_at_soft_wraps: true,
1693 };
1694
1695 let editor = cx.add_window(|window, cx| {
1696 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1697 build_editor(buffer, window, cx)
1698 });
1699 _ = editor.update(cx, |editor, window, cx| {
1700 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1701 s.select_display_ranges([
1702 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1703 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1704 ]);
1705 });
1706 });
1707
1708 _ = editor.update(cx, |editor, window, cx| {
1709 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1710 assert_eq!(
1711 editor.selections.display_ranges(cx),
1712 &[
1713 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1714 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1715 ]
1716 );
1717 });
1718
1719 _ = editor.update(cx, |editor, window, cx| {
1720 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1721 assert_eq!(
1722 editor.selections.display_ranges(cx),
1723 &[
1724 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1725 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1726 ]
1727 );
1728 });
1729
1730 _ = editor.update(cx, |editor, window, cx| {
1731 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1732 assert_eq!(
1733 editor.selections.display_ranges(cx),
1734 &[
1735 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1736 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1737 ]
1738 );
1739 });
1740
1741 _ = editor.update(cx, |editor, window, cx| {
1742 editor.move_to_end_of_line(&move_to_end, window, cx);
1743 assert_eq!(
1744 editor.selections.display_ranges(cx),
1745 &[
1746 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1747 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1748 ]
1749 );
1750 });
1751
1752 // Moving to the end of line again is a no-op.
1753 _ = editor.update(cx, |editor, window, cx| {
1754 editor.move_to_end_of_line(&move_to_end, window, cx);
1755 assert_eq!(
1756 editor.selections.display_ranges(cx),
1757 &[
1758 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1759 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1760 ]
1761 );
1762 });
1763
1764 _ = editor.update(cx, |editor, window, cx| {
1765 editor.move_left(&MoveLeft, window, cx);
1766 editor.select_to_beginning_of_line(
1767 &SelectToBeginningOfLine {
1768 stop_at_soft_wraps: true,
1769 stop_at_indent: true,
1770 },
1771 window,
1772 cx,
1773 );
1774 assert_eq!(
1775 editor.selections.display_ranges(cx),
1776 &[
1777 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1778 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1779 ]
1780 );
1781 });
1782
1783 _ = editor.update(cx, |editor, window, cx| {
1784 editor.select_to_beginning_of_line(
1785 &SelectToBeginningOfLine {
1786 stop_at_soft_wraps: true,
1787 stop_at_indent: true,
1788 },
1789 window,
1790 cx,
1791 );
1792 assert_eq!(
1793 editor.selections.display_ranges(cx),
1794 &[
1795 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1796 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1797 ]
1798 );
1799 });
1800
1801 _ = editor.update(cx, |editor, window, cx| {
1802 editor.select_to_beginning_of_line(
1803 &SelectToBeginningOfLine {
1804 stop_at_soft_wraps: true,
1805 stop_at_indent: true,
1806 },
1807 window,
1808 cx,
1809 );
1810 assert_eq!(
1811 editor.selections.display_ranges(cx),
1812 &[
1813 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1814 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1815 ]
1816 );
1817 });
1818
1819 _ = editor.update(cx, |editor, window, cx| {
1820 editor.select_to_end_of_line(
1821 &SelectToEndOfLine {
1822 stop_at_soft_wraps: true,
1823 },
1824 window,
1825 cx,
1826 );
1827 assert_eq!(
1828 editor.selections.display_ranges(cx),
1829 &[
1830 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1831 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1832 ]
1833 );
1834 });
1835
1836 _ = editor.update(cx, |editor, window, cx| {
1837 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1838 assert_eq!(editor.display_text(cx), "ab\n de");
1839 assert_eq!(
1840 editor.selections.display_ranges(cx),
1841 &[
1842 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1843 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1844 ]
1845 );
1846 });
1847
1848 _ = editor.update(cx, |editor, window, cx| {
1849 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1850 assert_eq!(editor.display_text(cx), "\n");
1851 assert_eq!(
1852 editor.selections.display_ranges(cx),
1853 &[
1854 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1855 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1856 ]
1857 );
1858 });
1859}
1860
1861#[gpui::test]
1862fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1863 init_test(cx, |_| {});
1864 let move_to_beg = MoveToBeginningOfLine {
1865 stop_at_soft_wraps: false,
1866 stop_at_indent: false,
1867 };
1868
1869 let move_to_end = MoveToEndOfLine {
1870 stop_at_soft_wraps: false,
1871 };
1872
1873 let editor = cx.add_window(|window, cx| {
1874 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1875 build_editor(buffer, window, cx)
1876 });
1877
1878 _ = editor.update(cx, |editor, window, cx| {
1879 editor.set_wrap_width(Some(140.0.into()), cx);
1880
1881 // We expect the following lines after wrapping
1882 // ```
1883 // thequickbrownfox
1884 // jumpedoverthelazydo
1885 // gs
1886 // ```
1887 // The final `gs` was soft-wrapped onto a new line.
1888 assert_eq!(
1889 "thequickbrownfox\njumpedoverthelaz\nydogs",
1890 editor.display_text(cx),
1891 );
1892
1893 // First, let's assert behavior on the first line, that was not soft-wrapped.
1894 // Start the cursor at the `k` on the first line
1895 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1896 s.select_display_ranges([
1897 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1898 ]);
1899 });
1900
1901 // Moving to the beginning of the line should put us at the beginning of the line.
1902 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1903 assert_eq!(
1904 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1905 editor.selections.display_ranges(cx)
1906 );
1907
1908 // Moving to the end of the line should put us at the end of the line.
1909 editor.move_to_end_of_line(&move_to_end, window, cx);
1910 assert_eq!(
1911 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1912 editor.selections.display_ranges(cx)
1913 );
1914
1915 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1916 // Start the cursor at the last line (`y` that was wrapped to a new line)
1917 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1918 s.select_display_ranges([
1919 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1920 ]);
1921 });
1922
1923 // Moving to the beginning of the line should put us at the start of the second line of
1924 // display text, i.e., the `j`.
1925 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1926 assert_eq!(
1927 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1928 editor.selections.display_ranges(cx)
1929 );
1930
1931 // Moving to the beginning of the line again should be a no-op.
1932 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1933 assert_eq!(
1934 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1935 editor.selections.display_ranges(cx)
1936 );
1937
1938 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1939 // next display line.
1940 editor.move_to_end_of_line(&move_to_end, window, cx);
1941 assert_eq!(
1942 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1943 editor.selections.display_ranges(cx)
1944 );
1945
1946 // Moving to the end of the line again should be a no-op.
1947 editor.move_to_end_of_line(&move_to_end, window, cx);
1948 assert_eq!(
1949 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1950 editor.selections.display_ranges(cx)
1951 );
1952 });
1953}
1954
1955#[gpui::test]
1956fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1957 init_test(cx, |_| {});
1958
1959 let move_to_beg = MoveToBeginningOfLine {
1960 stop_at_soft_wraps: true,
1961 stop_at_indent: true,
1962 };
1963
1964 let select_to_beg = SelectToBeginningOfLine {
1965 stop_at_soft_wraps: true,
1966 stop_at_indent: true,
1967 };
1968
1969 let delete_to_beg = DeleteToBeginningOfLine {
1970 stop_at_indent: true,
1971 };
1972
1973 let move_to_end = MoveToEndOfLine {
1974 stop_at_soft_wraps: false,
1975 };
1976
1977 let editor = cx.add_window(|window, cx| {
1978 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1979 build_editor(buffer, window, cx)
1980 });
1981
1982 _ = editor.update(cx, |editor, window, cx| {
1983 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1984 s.select_display_ranges([
1985 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1986 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1987 ]);
1988 });
1989
1990 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1991 // and the second cursor at the first non-whitespace character in the line.
1992 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1993 assert_eq!(
1994 editor.selections.display_ranges(cx),
1995 &[
1996 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1997 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1998 ]
1999 );
2000
2001 // Moving to the beginning of the line again should be a no-op for the first cursor,
2002 // and should move the second cursor to the beginning of the line.
2003 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2004 assert_eq!(
2005 editor.selections.display_ranges(cx),
2006 &[
2007 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2008 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
2009 ]
2010 );
2011
2012 // Moving to the beginning of the line again should still be a no-op for the first cursor,
2013 // and should move the second cursor back to the first non-whitespace character in the line.
2014 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2015 assert_eq!(
2016 editor.selections.display_ranges(cx),
2017 &[
2018 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2019 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2020 ]
2021 );
2022
2023 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
2024 // and to the first non-whitespace character in the line for the second cursor.
2025 editor.move_to_end_of_line(&move_to_end, window, cx);
2026 editor.move_left(&MoveLeft, window, cx);
2027 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2028 assert_eq!(
2029 editor.selections.display_ranges(cx),
2030 &[
2031 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2032 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2033 ]
2034 );
2035
2036 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2037 // and should select to the beginning of the line for the second cursor.
2038 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2039 assert_eq!(
2040 editor.selections.display_ranges(cx),
2041 &[
2042 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2043 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2044 ]
2045 );
2046
2047 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2048 // and should delete to the first non-whitespace character in the line for the second cursor.
2049 editor.move_to_end_of_line(&move_to_end, window, cx);
2050 editor.move_left(&MoveLeft, window, cx);
2051 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2052 assert_eq!(editor.text(cx), "c\n f");
2053 });
2054}
2055
2056#[gpui::test]
2057fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2058 init_test(cx, |_| {});
2059
2060 let move_to_beg = MoveToBeginningOfLine {
2061 stop_at_soft_wraps: true,
2062 stop_at_indent: true,
2063 };
2064
2065 let editor = cx.add_window(|window, cx| {
2066 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2067 build_editor(buffer, window, cx)
2068 });
2069
2070 _ = editor.update(cx, |editor, window, cx| {
2071 // test cursor between line_start and indent_start
2072 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2073 s.select_display_ranges([
2074 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2075 ]);
2076 });
2077
2078 // cursor should move to line_start
2079 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2080 assert_eq!(
2081 editor.selections.display_ranges(cx),
2082 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2083 );
2084
2085 // cursor should move to indent_start
2086 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2087 assert_eq!(
2088 editor.selections.display_ranges(cx),
2089 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2090 );
2091
2092 // cursor should move to back to line_start
2093 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2094 assert_eq!(
2095 editor.selections.display_ranges(cx),
2096 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2097 );
2098 });
2099}
2100
2101#[gpui::test]
2102fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2103 init_test(cx, |_| {});
2104
2105 let editor = cx.add_window(|window, cx| {
2106 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2107 build_editor(buffer, window, cx)
2108 });
2109 _ = editor.update(cx, |editor, window, cx| {
2110 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2111 s.select_display_ranges([
2112 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2113 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2114 ])
2115 });
2116 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2117 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2118
2119 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2120 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2121
2122 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2123 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2124
2125 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2126 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2127
2128 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2129 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2130
2131 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2132 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2133
2134 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2135 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2136
2137 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2138 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2139
2140 editor.move_right(&MoveRight, window, cx);
2141 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2142 assert_selection_ranges(
2143 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2144 editor,
2145 cx,
2146 );
2147
2148 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2149 assert_selection_ranges(
2150 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2151 editor,
2152 cx,
2153 );
2154
2155 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2156 assert_selection_ranges(
2157 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2158 editor,
2159 cx,
2160 );
2161 });
2162}
2163
2164#[gpui::test]
2165fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2166 init_test(cx, |_| {});
2167
2168 let editor = cx.add_window(|window, cx| {
2169 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2170 build_editor(buffer, window, cx)
2171 });
2172
2173 _ = editor.update(cx, |editor, window, cx| {
2174 editor.set_wrap_width(Some(140.0.into()), cx);
2175 assert_eq!(
2176 editor.display_text(cx),
2177 "use one::{\n two::three::\n four::five\n};"
2178 );
2179
2180 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2181 s.select_display_ranges([
2182 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2183 ]);
2184 });
2185
2186 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2187 assert_eq!(
2188 editor.selections.display_ranges(cx),
2189 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2190 );
2191
2192 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2193 assert_eq!(
2194 editor.selections.display_ranges(cx),
2195 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2196 );
2197
2198 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2199 assert_eq!(
2200 editor.selections.display_ranges(cx),
2201 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2202 );
2203
2204 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2205 assert_eq!(
2206 editor.selections.display_ranges(cx),
2207 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2208 );
2209
2210 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2211 assert_eq!(
2212 editor.selections.display_ranges(cx),
2213 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2214 );
2215
2216 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2217 assert_eq!(
2218 editor.selections.display_ranges(cx),
2219 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2220 );
2221 });
2222}
2223
2224#[gpui::test]
2225async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2226 init_test(cx, |_| {});
2227 let mut cx = EditorTestContext::new(cx).await;
2228
2229 let line_height = cx.editor(|editor, window, _| {
2230 editor
2231 .style()
2232 .unwrap()
2233 .text
2234 .line_height_in_pixels(window.rem_size())
2235 });
2236 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2237
2238 cx.set_state(
2239 &r#"ˇone
2240 two
2241
2242 three
2243 fourˇ
2244 five
2245
2246 six"#
2247 .unindent(),
2248 );
2249
2250 cx.update_editor(|editor, window, cx| {
2251 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2252 });
2253 cx.assert_editor_state(
2254 &r#"one
2255 two
2256 ˇ
2257 three
2258 four
2259 five
2260 ˇ
2261 six"#
2262 .unindent(),
2263 );
2264
2265 cx.update_editor(|editor, window, cx| {
2266 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2267 });
2268 cx.assert_editor_state(
2269 &r#"one
2270 two
2271
2272 three
2273 four
2274 five
2275 ˇ
2276 sixˇ"#
2277 .unindent(),
2278 );
2279
2280 cx.update_editor(|editor, window, cx| {
2281 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2282 });
2283 cx.assert_editor_state(
2284 &r#"one
2285 two
2286
2287 three
2288 four
2289 five
2290
2291 sixˇ"#
2292 .unindent(),
2293 );
2294
2295 cx.update_editor(|editor, window, cx| {
2296 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2297 });
2298 cx.assert_editor_state(
2299 &r#"one
2300 two
2301
2302 three
2303 four
2304 five
2305 ˇ
2306 six"#
2307 .unindent(),
2308 );
2309
2310 cx.update_editor(|editor, window, cx| {
2311 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2312 });
2313 cx.assert_editor_state(
2314 &r#"one
2315 two
2316 ˇ
2317 three
2318 four
2319 five
2320
2321 six"#
2322 .unindent(),
2323 );
2324
2325 cx.update_editor(|editor, window, cx| {
2326 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2327 });
2328 cx.assert_editor_state(
2329 &r#"ˇone
2330 two
2331
2332 three
2333 four
2334 five
2335
2336 six"#
2337 .unindent(),
2338 );
2339}
2340
2341#[gpui::test]
2342async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2343 init_test(cx, |_| {});
2344 let mut cx = EditorTestContext::new(cx).await;
2345 let line_height = cx.editor(|editor, window, _| {
2346 editor
2347 .style()
2348 .unwrap()
2349 .text
2350 .line_height_in_pixels(window.rem_size())
2351 });
2352 let window = cx.window;
2353 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2354
2355 cx.set_state(
2356 r#"ˇone
2357 two
2358 three
2359 four
2360 five
2361 six
2362 seven
2363 eight
2364 nine
2365 ten
2366 "#,
2367 );
2368
2369 cx.update_editor(|editor, window, cx| {
2370 assert_eq!(
2371 editor.snapshot(window, cx).scroll_position(),
2372 gpui::Point::new(0., 0.)
2373 );
2374 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2375 assert_eq!(
2376 editor.snapshot(window, cx).scroll_position(),
2377 gpui::Point::new(0., 3.)
2378 );
2379 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2380 assert_eq!(
2381 editor.snapshot(window, cx).scroll_position(),
2382 gpui::Point::new(0., 6.)
2383 );
2384 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2385 assert_eq!(
2386 editor.snapshot(window, cx).scroll_position(),
2387 gpui::Point::new(0., 3.)
2388 );
2389
2390 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2391 assert_eq!(
2392 editor.snapshot(window, cx).scroll_position(),
2393 gpui::Point::new(0., 1.)
2394 );
2395 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2396 assert_eq!(
2397 editor.snapshot(window, cx).scroll_position(),
2398 gpui::Point::new(0., 3.)
2399 );
2400 });
2401}
2402
2403#[gpui::test]
2404async fn test_autoscroll(cx: &mut TestAppContext) {
2405 init_test(cx, |_| {});
2406 let mut cx = EditorTestContext::new(cx).await;
2407
2408 let line_height = cx.update_editor(|editor, window, cx| {
2409 editor.set_vertical_scroll_margin(2, cx);
2410 editor
2411 .style()
2412 .unwrap()
2413 .text
2414 .line_height_in_pixels(window.rem_size())
2415 });
2416 let window = cx.window;
2417 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2418
2419 cx.set_state(
2420 r#"ˇone
2421 two
2422 three
2423 four
2424 five
2425 six
2426 seven
2427 eight
2428 nine
2429 ten
2430 "#,
2431 );
2432 cx.update_editor(|editor, window, cx| {
2433 assert_eq!(
2434 editor.snapshot(window, cx).scroll_position(),
2435 gpui::Point::new(0., 0.0)
2436 );
2437 });
2438
2439 // Add a cursor below the visible area. Since both cursors cannot fit
2440 // on screen, the editor autoscrolls to reveal the newest cursor, and
2441 // allows the vertical scroll margin below that cursor.
2442 cx.update_editor(|editor, window, cx| {
2443 editor.change_selections(Default::default(), window, cx, |selections| {
2444 selections.select_ranges([
2445 Point::new(0, 0)..Point::new(0, 0),
2446 Point::new(6, 0)..Point::new(6, 0),
2447 ]);
2448 })
2449 });
2450 cx.update_editor(|editor, window, cx| {
2451 assert_eq!(
2452 editor.snapshot(window, cx).scroll_position(),
2453 gpui::Point::new(0., 3.0)
2454 );
2455 });
2456
2457 // Move down. The editor cursor scrolls down to track the newest cursor.
2458 cx.update_editor(|editor, window, cx| {
2459 editor.move_down(&Default::default(), window, cx);
2460 });
2461 cx.update_editor(|editor, window, cx| {
2462 assert_eq!(
2463 editor.snapshot(window, cx).scroll_position(),
2464 gpui::Point::new(0., 4.0)
2465 );
2466 });
2467
2468 // Add a cursor above the visible area. Since both cursors fit on screen,
2469 // the editor scrolls to show both.
2470 cx.update_editor(|editor, window, cx| {
2471 editor.change_selections(Default::default(), window, cx, |selections| {
2472 selections.select_ranges([
2473 Point::new(1, 0)..Point::new(1, 0),
2474 Point::new(6, 0)..Point::new(6, 0),
2475 ]);
2476 })
2477 });
2478 cx.update_editor(|editor, window, cx| {
2479 assert_eq!(
2480 editor.snapshot(window, cx).scroll_position(),
2481 gpui::Point::new(0., 1.0)
2482 );
2483 });
2484}
2485
2486#[gpui::test]
2487async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2488 init_test(cx, |_| {});
2489 let mut cx = EditorTestContext::new(cx).await;
2490
2491 let line_height = cx.editor(|editor, window, _cx| {
2492 editor
2493 .style()
2494 .unwrap()
2495 .text
2496 .line_height_in_pixels(window.rem_size())
2497 });
2498 let window = cx.window;
2499 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2500 cx.set_state(
2501 &r#"
2502 ˇone
2503 two
2504 threeˇ
2505 four
2506 five
2507 six
2508 seven
2509 eight
2510 nine
2511 ten
2512 "#
2513 .unindent(),
2514 );
2515
2516 cx.update_editor(|editor, window, cx| {
2517 editor.move_page_down(&MovePageDown::default(), window, cx)
2518 });
2519 cx.assert_editor_state(
2520 &r#"
2521 one
2522 two
2523 three
2524 ˇfour
2525 five
2526 sixˇ
2527 seven
2528 eight
2529 nine
2530 ten
2531 "#
2532 .unindent(),
2533 );
2534
2535 cx.update_editor(|editor, window, cx| {
2536 editor.move_page_down(&MovePageDown::default(), window, cx)
2537 });
2538 cx.assert_editor_state(
2539 &r#"
2540 one
2541 two
2542 three
2543 four
2544 five
2545 six
2546 ˇseven
2547 eight
2548 nineˇ
2549 ten
2550 "#
2551 .unindent(),
2552 );
2553
2554 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2555 cx.assert_editor_state(
2556 &r#"
2557 one
2558 two
2559 three
2560 ˇfour
2561 five
2562 sixˇ
2563 seven
2564 eight
2565 nine
2566 ten
2567 "#
2568 .unindent(),
2569 );
2570
2571 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2572 cx.assert_editor_state(
2573 &r#"
2574 ˇone
2575 two
2576 threeˇ
2577 four
2578 five
2579 six
2580 seven
2581 eight
2582 nine
2583 ten
2584 "#
2585 .unindent(),
2586 );
2587
2588 // Test select collapsing
2589 cx.update_editor(|editor, window, cx| {
2590 editor.move_page_down(&MovePageDown::default(), window, cx);
2591 editor.move_page_down(&MovePageDown::default(), window, cx);
2592 editor.move_page_down(&MovePageDown::default(), window, cx);
2593 });
2594 cx.assert_editor_state(
2595 &r#"
2596 one
2597 two
2598 three
2599 four
2600 five
2601 six
2602 seven
2603 eight
2604 nine
2605 ˇten
2606 ˇ"#
2607 .unindent(),
2608 );
2609}
2610
2611#[gpui::test]
2612async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2613 init_test(cx, |_| {});
2614 let mut cx = EditorTestContext::new(cx).await;
2615 cx.set_state("one «two threeˇ» four");
2616 cx.update_editor(|editor, window, cx| {
2617 editor.delete_to_beginning_of_line(
2618 &DeleteToBeginningOfLine {
2619 stop_at_indent: false,
2620 },
2621 window,
2622 cx,
2623 );
2624 assert_eq!(editor.text(cx), " four");
2625 });
2626}
2627
2628#[gpui::test]
2629async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2630 init_test(cx, |_| {});
2631
2632 let mut cx = EditorTestContext::new(cx).await;
2633
2634 // For an empty selection, the preceding word fragment is deleted.
2635 // For non-empty selections, only selected characters are deleted.
2636 cx.set_state("onˇe two t«hreˇ»e four");
2637 cx.update_editor(|editor, window, cx| {
2638 editor.delete_to_previous_word_start(
2639 &DeleteToPreviousWordStart {
2640 ignore_newlines: false,
2641 ignore_brackets: false,
2642 },
2643 window,
2644 cx,
2645 );
2646 });
2647 cx.assert_editor_state("ˇe two tˇe four");
2648
2649 cx.set_state("e tˇwo te «fˇ»our");
2650 cx.update_editor(|editor, window, cx| {
2651 editor.delete_to_next_word_end(
2652 &DeleteToNextWordEnd {
2653 ignore_newlines: false,
2654 ignore_brackets: false,
2655 },
2656 window,
2657 cx,
2658 );
2659 });
2660 cx.assert_editor_state("e tˇ te ˇour");
2661}
2662
2663#[gpui::test]
2664async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2665 init_test(cx, |_| {});
2666
2667 let mut cx = EditorTestContext::new(cx).await;
2668
2669 cx.set_state("here is some text ˇwith a space");
2670 cx.update_editor(|editor, window, cx| {
2671 editor.delete_to_previous_word_start(
2672 &DeleteToPreviousWordStart {
2673 ignore_newlines: false,
2674 ignore_brackets: true,
2675 },
2676 window,
2677 cx,
2678 );
2679 });
2680 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2681 cx.assert_editor_state("here is some textˇwith a space");
2682
2683 cx.set_state("here is some text ˇwith a space");
2684 cx.update_editor(|editor, window, cx| {
2685 editor.delete_to_previous_word_start(
2686 &DeleteToPreviousWordStart {
2687 ignore_newlines: false,
2688 ignore_brackets: false,
2689 },
2690 window,
2691 cx,
2692 );
2693 });
2694 cx.assert_editor_state("here is some textˇwith a space");
2695
2696 cx.set_state("here is some textˇ with a space");
2697 cx.update_editor(|editor, window, cx| {
2698 editor.delete_to_next_word_end(
2699 &DeleteToNextWordEnd {
2700 ignore_newlines: false,
2701 ignore_brackets: true,
2702 },
2703 window,
2704 cx,
2705 );
2706 });
2707 // Same happens in the other direction.
2708 cx.assert_editor_state("here is some textˇwith a space");
2709
2710 cx.set_state("here is some textˇ with a space");
2711 cx.update_editor(|editor, window, cx| {
2712 editor.delete_to_next_word_end(
2713 &DeleteToNextWordEnd {
2714 ignore_newlines: false,
2715 ignore_brackets: false,
2716 },
2717 window,
2718 cx,
2719 );
2720 });
2721 cx.assert_editor_state("here is some textˇwith a space");
2722
2723 cx.set_state("here is some textˇ with a space");
2724 cx.update_editor(|editor, window, cx| {
2725 editor.delete_to_next_word_end(
2726 &DeleteToNextWordEnd {
2727 ignore_newlines: true,
2728 ignore_brackets: false,
2729 },
2730 window,
2731 cx,
2732 );
2733 });
2734 cx.assert_editor_state("here is some textˇwith a space");
2735 cx.update_editor(|editor, window, cx| {
2736 editor.delete_to_previous_word_start(
2737 &DeleteToPreviousWordStart {
2738 ignore_newlines: true,
2739 ignore_brackets: false,
2740 },
2741 window,
2742 cx,
2743 );
2744 });
2745 cx.assert_editor_state("here is some ˇwith a space");
2746 cx.update_editor(|editor, window, cx| {
2747 editor.delete_to_previous_word_start(
2748 &DeleteToPreviousWordStart {
2749 ignore_newlines: true,
2750 ignore_brackets: false,
2751 },
2752 window,
2753 cx,
2754 );
2755 });
2756 // Single whitespaces are removed with the word behind them.
2757 cx.assert_editor_state("here is ˇwith a space");
2758 cx.update_editor(|editor, window, cx| {
2759 editor.delete_to_previous_word_start(
2760 &DeleteToPreviousWordStart {
2761 ignore_newlines: true,
2762 ignore_brackets: false,
2763 },
2764 window,
2765 cx,
2766 );
2767 });
2768 cx.assert_editor_state("here ˇwith a space");
2769 cx.update_editor(|editor, window, cx| {
2770 editor.delete_to_previous_word_start(
2771 &DeleteToPreviousWordStart {
2772 ignore_newlines: true,
2773 ignore_brackets: false,
2774 },
2775 window,
2776 cx,
2777 );
2778 });
2779 cx.assert_editor_state("ˇwith a space");
2780 cx.update_editor(|editor, window, cx| {
2781 editor.delete_to_previous_word_start(
2782 &DeleteToPreviousWordStart {
2783 ignore_newlines: true,
2784 ignore_brackets: false,
2785 },
2786 window,
2787 cx,
2788 );
2789 });
2790 cx.assert_editor_state("ˇwith a space");
2791 cx.update_editor(|editor, window, cx| {
2792 editor.delete_to_next_word_end(
2793 &DeleteToNextWordEnd {
2794 ignore_newlines: true,
2795 ignore_brackets: false,
2796 },
2797 window,
2798 cx,
2799 );
2800 });
2801 // Same happens in the other direction.
2802 cx.assert_editor_state("ˇ a space");
2803 cx.update_editor(|editor, window, cx| {
2804 editor.delete_to_next_word_end(
2805 &DeleteToNextWordEnd {
2806 ignore_newlines: true,
2807 ignore_brackets: false,
2808 },
2809 window,
2810 cx,
2811 );
2812 });
2813 cx.assert_editor_state("ˇ space");
2814 cx.update_editor(|editor, window, cx| {
2815 editor.delete_to_next_word_end(
2816 &DeleteToNextWordEnd {
2817 ignore_newlines: true,
2818 ignore_brackets: false,
2819 },
2820 window,
2821 cx,
2822 );
2823 });
2824 cx.assert_editor_state("ˇ");
2825 cx.update_editor(|editor, window, cx| {
2826 editor.delete_to_next_word_end(
2827 &DeleteToNextWordEnd {
2828 ignore_newlines: true,
2829 ignore_brackets: false,
2830 },
2831 window,
2832 cx,
2833 );
2834 });
2835 cx.assert_editor_state("ˇ");
2836 cx.update_editor(|editor, window, cx| {
2837 editor.delete_to_previous_word_start(
2838 &DeleteToPreviousWordStart {
2839 ignore_newlines: true,
2840 ignore_brackets: false,
2841 },
2842 window,
2843 cx,
2844 );
2845 });
2846 cx.assert_editor_state("ˇ");
2847}
2848
2849#[gpui::test]
2850async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2851 init_test(cx, |_| {});
2852
2853 let language = Arc::new(
2854 Language::new(
2855 LanguageConfig {
2856 brackets: BracketPairConfig {
2857 pairs: vec![
2858 BracketPair {
2859 start: "\"".to_string(),
2860 end: "\"".to_string(),
2861 close: true,
2862 surround: true,
2863 newline: false,
2864 },
2865 BracketPair {
2866 start: "(".to_string(),
2867 end: ")".to_string(),
2868 close: true,
2869 surround: true,
2870 newline: true,
2871 },
2872 ],
2873 ..BracketPairConfig::default()
2874 },
2875 ..LanguageConfig::default()
2876 },
2877 Some(tree_sitter_rust::LANGUAGE.into()),
2878 )
2879 .with_brackets_query(
2880 r#"
2881 ("(" @open ")" @close)
2882 ("\"" @open "\"" @close)
2883 "#,
2884 )
2885 .unwrap(),
2886 );
2887
2888 let mut cx = EditorTestContext::new(cx).await;
2889 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2890
2891 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2892 cx.update_editor(|editor, window, cx| {
2893 editor.delete_to_previous_word_start(
2894 &DeleteToPreviousWordStart {
2895 ignore_newlines: true,
2896 ignore_brackets: false,
2897 },
2898 window,
2899 cx,
2900 );
2901 });
2902 // Deletion stops before brackets if asked to not ignore them.
2903 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2904 cx.update_editor(|editor, window, cx| {
2905 editor.delete_to_previous_word_start(
2906 &DeleteToPreviousWordStart {
2907 ignore_newlines: true,
2908 ignore_brackets: false,
2909 },
2910 window,
2911 cx,
2912 );
2913 });
2914 // Deletion has to remove a single bracket and then stop again.
2915 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2916
2917 cx.update_editor(|editor, window, cx| {
2918 editor.delete_to_previous_word_start(
2919 &DeleteToPreviousWordStart {
2920 ignore_newlines: true,
2921 ignore_brackets: false,
2922 },
2923 window,
2924 cx,
2925 );
2926 });
2927 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2928
2929 cx.update_editor(|editor, window, cx| {
2930 editor.delete_to_previous_word_start(
2931 &DeleteToPreviousWordStart {
2932 ignore_newlines: true,
2933 ignore_brackets: false,
2934 },
2935 window,
2936 cx,
2937 );
2938 });
2939 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2940
2941 cx.update_editor(|editor, window, cx| {
2942 editor.delete_to_previous_word_start(
2943 &DeleteToPreviousWordStart {
2944 ignore_newlines: true,
2945 ignore_brackets: false,
2946 },
2947 window,
2948 cx,
2949 );
2950 });
2951 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2952
2953 cx.update_editor(|editor, window, cx| {
2954 editor.delete_to_next_word_end(
2955 &DeleteToNextWordEnd {
2956 ignore_newlines: true,
2957 ignore_brackets: false,
2958 },
2959 window,
2960 cx,
2961 );
2962 });
2963 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2964 cx.assert_editor_state(r#"ˇ");"#);
2965
2966 cx.update_editor(|editor, window, cx| {
2967 editor.delete_to_next_word_end(
2968 &DeleteToNextWordEnd {
2969 ignore_newlines: true,
2970 ignore_brackets: false,
2971 },
2972 window,
2973 cx,
2974 );
2975 });
2976 cx.assert_editor_state(r#"ˇ"#);
2977
2978 cx.update_editor(|editor, window, cx| {
2979 editor.delete_to_next_word_end(
2980 &DeleteToNextWordEnd {
2981 ignore_newlines: true,
2982 ignore_brackets: false,
2983 },
2984 window,
2985 cx,
2986 );
2987 });
2988 cx.assert_editor_state(r#"ˇ"#);
2989
2990 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2991 cx.update_editor(|editor, window, cx| {
2992 editor.delete_to_previous_word_start(
2993 &DeleteToPreviousWordStart {
2994 ignore_newlines: true,
2995 ignore_brackets: true,
2996 },
2997 window,
2998 cx,
2999 );
3000 });
3001 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
3002}
3003
3004#[gpui::test]
3005fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
3006 init_test(cx, |_| {});
3007
3008 let editor = cx.add_window(|window, cx| {
3009 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
3010 build_editor(buffer, window, cx)
3011 });
3012 let del_to_prev_word_start = DeleteToPreviousWordStart {
3013 ignore_newlines: false,
3014 ignore_brackets: false,
3015 };
3016 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
3017 ignore_newlines: true,
3018 ignore_brackets: false,
3019 };
3020
3021 _ = editor.update(cx, |editor, window, cx| {
3022 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3023 s.select_display_ranges([
3024 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
3025 ])
3026 });
3027 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3028 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
3029 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3030 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
3031 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3032 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
3033 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3034 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
3035 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3036 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3037 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3038 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3039 });
3040}
3041
3042#[gpui::test]
3043fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3044 init_test(cx, |_| {});
3045
3046 let editor = cx.add_window(|window, cx| {
3047 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3048 build_editor(buffer, window, cx)
3049 });
3050 let del_to_next_word_end = DeleteToNextWordEnd {
3051 ignore_newlines: false,
3052 ignore_brackets: false,
3053 };
3054 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3055 ignore_newlines: true,
3056 ignore_brackets: false,
3057 };
3058
3059 _ = editor.update(cx, |editor, window, cx| {
3060 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3061 s.select_display_ranges([
3062 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3063 ])
3064 });
3065 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3066 assert_eq!(
3067 editor.buffer.read(cx).read(cx).text(),
3068 "one\n two\nthree\n four"
3069 );
3070 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3071 assert_eq!(
3072 editor.buffer.read(cx).read(cx).text(),
3073 "\n two\nthree\n four"
3074 );
3075 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3076 assert_eq!(
3077 editor.buffer.read(cx).read(cx).text(),
3078 "two\nthree\n four"
3079 );
3080 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3081 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3082 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3083 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3084 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3085 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3086 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3087 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3088 });
3089}
3090
3091#[gpui::test]
3092fn test_newline(cx: &mut TestAppContext) {
3093 init_test(cx, |_| {});
3094
3095 let editor = cx.add_window(|window, cx| {
3096 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3097 build_editor(buffer, window, cx)
3098 });
3099
3100 _ = editor.update(cx, |editor, window, cx| {
3101 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3102 s.select_display_ranges([
3103 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3104 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3105 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3106 ])
3107 });
3108
3109 editor.newline(&Newline, window, cx);
3110 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3111 });
3112}
3113
3114#[gpui::test]
3115fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3116 init_test(cx, |_| {});
3117
3118 let editor = cx.add_window(|window, cx| {
3119 let buffer = MultiBuffer::build_simple(
3120 "
3121 a
3122 b(
3123 X
3124 )
3125 c(
3126 X
3127 )
3128 "
3129 .unindent()
3130 .as_str(),
3131 cx,
3132 );
3133 let mut editor = build_editor(buffer, window, cx);
3134 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3135 s.select_ranges([
3136 Point::new(2, 4)..Point::new(2, 5),
3137 Point::new(5, 4)..Point::new(5, 5),
3138 ])
3139 });
3140 editor
3141 });
3142
3143 _ = editor.update(cx, |editor, window, cx| {
3144 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3145 editor.buffer.update(cx, |buffer, cx| {
3146 buffer.edit(
3147 [
3148 (Point::new(1, 2)..Point::new(3, 0), ""),
3149 (Point::new(4, 2)..Point::new(6, 0), ""),
3150 ],
3151 None,
3152 cx,
3153 );
3154 assert_eq!(
3155 buffer.read(cx).text(),
3156 "
3157 a
3158 b()
3159 c()
3160 "
3161 .unindent()
3162 );
3163 });
3164 assert_eq!(
3165 editor.selections.ranges(cx),
3166 &[
3167 Point::new(1, 2)..Point::new(1, 2),
3168 Point::new(2, 2)..Point::new(2, 2),
3169 ],
3170 );
3171
3172 editor.newline(&Newline, window, cx);
3173 assert_eq!(
3174 editor.text(cx),
3175 "
3176 a
3177 b(
3178 )
3179 c(
3180 )
3181 "
3182 .unindent()
3183 );
3184
3185 // The selections are moved after the inserted newlines
3186 assert_eq!(
3187 editor.selections.ranges(cx),
3188 &[
3189 Point::new(2, 0)..Point::new(2, 0),
3190 Point::new(4, 0)..Point::new(4, 0),
3191 ],
3192 );
3193 });
3194}
3195
3196#[gpui::test]
3197async fn test_newline_above(cx: &mut TestAppContext) {
3198 init_test(cx, |settings| {
3199 settings.defaults.tab_size = NonZeroU32::new(4)
3200 });
3201
3202 let language = Arc::new(
3203 Language::new(
3204 LanguageConfig::default(),
3205 Some(tree_sitter_rust::LANGUAGE.into()),
3206 )
3207 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3208 .unwrap(),
3209 );
3210
3211 let mut cx = EditorTestContext::new(cx).await;
3212 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3213 cx.set_state(indoc! {"
3214 const a: ˇA = (
3215 (ˇ
3216 «const_functionˇ»(ˇ),
3217 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3218 )ˇ
3219 ˇ);ˇ
3220 "});
3221
3222 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3223 cx.assert_editor_state(indoc! {"
3224 ˇ
3225 const a: A = (
3226 ˇ
3227 (
3228 ˇ
3229 ˇ
3230 const_function(),
3231 ˇ
3232 ˇ
3233 ˇ
3234 ˇ
3235 something_else,
3236 ˇ
3237 )
3238 ˇ
3239 ˇ
3240 );
3241 "});
3242}
3243
3244#[gpui::test]
3245async fn test_newline_below(cx: &mut TestAppContext) {
3246 init_test(cx, |settings| {
3247 settings.defaults.tab_size = NonZeroU32::new(4)
3248 });
3249
3250 let language = Arc::new(
3251 Language::new(
3252 LanguageConfig::default(),
3253 Some(tree_sitter_rust::LANGUAGE.into()),
3254 )
3255 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3256 .unwrap(),
3257 );
3258
3259 let mut cx = EditorTestContext::new(cx).await;
3260 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3261 cx.set_state(indoc! {"
3262 const a: ˇA = (
3263 (ˇ
3264 «const_functionˇ»(ˇ),
3265 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3266 )ˇ
3267 ˇ);ˇ
3268 "});
3269
3270 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3271 cx.assert_editor_state(indoc! {"
3272 const a: A = (
3273 ˇ
3274 (
3275 ˇ
3276 const_function(),
3277 ˇ
3278 ˇ
3279 something_else,
3280 ˇ
3281 ˇ
3282 ˇ
3283 ˇ
3284 )
3285 ˇ
3286 );
3287 ˇ
3288 ˇ
3289 "});
3290}
3291
3292#[gpui::test]
3293async fn test_newline_comments(cx: &mut TestAppContext) {
3294 init_test(cx, |settings| {
3295 settings.defaults.tab_size = NonZeroU32::new(4)
3296 });
3297
3298 let language = Arc::new(Language::new(
3299 LanguageConfig {
3300 line_comments: vec!["// ".into()],
3301 ..LanguageConfig::default()
3302 },
3303 None,
3304 ));
3305 {
3306 let mut cx = EditorTestContext::new(cx).await;
3307 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3308 cx.set_state(indoc! {"
3309 // Fooˇ
3310 "});
3311
3312 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3313 cx.assert_editor_state(indoc! {"
3314 // Foo
3315 // ˇ
3316 "});
3317 // Ensure that we add comment prefix when existing line contains space
3318 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3319 cx.assert_editor_state(
3320 indoc! {"
3321 // Foo
3322 //s
3323 // ˇ
3324 "}
3325 .replace("s", " ") // s is used as space placeholder to prevent format on save
3326 .as_str(),
3327 );
3328 // Ensure that we add comment prefix when existing line does not contain space
3329 cx.set_state(indoc! {"
3330 // Foo
3331 //ˇ
3332 "});
3333 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3334 cx.assert_editor_state(indoc! {"
3335 // Foo
3336 //
3337 // ˇ
3338 "});
3339 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3340 cx.set_state(indoc! {"
3341 ˇ// Foo
3342 "});
3343 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3344 cx.assert_editor_state(indoc! {"
3345
3346 ˇ// Foo
3347 "});
3348 }
3349 // Ensure that comment continuations can be disabled.
3350 update_test_language_settings(cx, |settings| {
3351 settings.defaults.extend_comment_on_newline = Some(false);
3352 });
3353 let mut cx = EditorTestContext::new(cx).await;
3354 cx.set_state(indoc! {"
3355 // Fooˇ
3356 "});
3357 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3358 cx.assert_editor_state(indoc! {"
3359 // Foo
3360 ˇ
3361 "});
3362}
3363
3364#[gpui::test]
3365async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3366 init_test(cx, |settings| {
3367 settings.defaults.tab_size = NonZeroU32::new(4)
3368 });
3369
3370 let language = Arc::new(Language::new(
3371 LanguageConfig {
3372 line_comments: vec!["// ".into(), "/// ".into()],
3373 ..LanguageConfig::default()
3374 },
3375 None,
3376 ));
3377 {
3378 let mut cx = EditorTestContext::new(cx).await;
3379 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3380 cx.set_state(indoc! {"
3381 //ˇ
3382 "});
3383 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3384 cx.assert_editor_state(indoc! {"
3385 //
3386 // ˇ
3387 "});
3388
3389 cx.set_state(indoc! {"
3390 ///ˇ
3391 "});
3392 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3393 cx.assert_editor_state(indoc! {"
3394 ///
3395 /// ˇ
3396 "});
3397 }
3398}
3399
3400#[gpui::test]
3401async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3402 init_test(cx, |settings| {
3403 settings.defaults.tab_size = NonZeroU32::new(4)
3404 });
3405
3406 let language = Arc::new(
3407 Language::new(
3408 LanguageConfig {
3409 documentation_comment: Some(language::BlockCommentConfig {
3410 start: "/**".into(),
3411 end: "*/".into(),
3412 prefix: "* ".into(),
3413 tab_size: 1,
3414 }),
3415
3416 ..LanguageConfig::default()
3417 },
3418 Some(tree_sitter_rust::LANGUAGE.into()),
3419 )
3420 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3421 .unwrap(),
3422 );
3423
3424 {
3425 let mut cx = EditorTestContext::new(cx).await;
3426 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3427 cx.set_state(indoc! {"
3428 /**ˇ
3429 "});
3430
3431 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3432 cx.assert_editor_state(indoc! {"
3433 /**
3434 * ˇ
3435 "});
3436 // Ensure that if cursor is before the comment start,
3437 // we do not actually insert a comment prefix.
3438 cx.set_state(indoc! {"
3439 ˇ/**
3440 "});
3441 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3442 cx.assert_editor_state(indoc! {"
3443
3444 ˇ/**
3445 "});
3446 // Ensure that if cursor is between it doesn't add comment prefix.
3447 cx.set_state(indoc! {"
3448 /*ˇ*
3449 "});
3450 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3451 cx.assert_editor_state(indoc! {"
3452 /*
3453 ˇ*
3454 "});
3455 // Ensure that if suffix exists on same line after cursor it adds new line.
3456 cx.set_state(indoc! {"
3457 /**ˇ*/
3458 "});
3459 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3460 cx.assert_editor_state(indoc! {"
3461 /**
3462 * ˇ
3463 */
3464 "});
3465 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3466 cx.set_state(indoc! {"
3467 /**ˇ */
3468 "});
3469 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3470 cx.assert_editor_state(indoc! {"
3471 /**
3472 * ˇ
3473 */
3474 "});
3475 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3476 cx.set_state(indoc! {"
3477 /** ˇ*/
3478 "});
3479 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3480 cx.assert_editor_state(
3481 indoc! {"
3482 /**s
3483 * ˇ
3484 */
3485 "}
3486 .replace("s", " ") // s is used as space placeholder to prevent format on save
3487 .as_str(),
3488 );
3489 // Ensure that delimiter space is preserved when newline on already
3490 // spaced delimiter.
3491 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3492 cx.assert_editor_state(
3493 indoc! {"
3494 /**s
3495 *s
3496 * ˇ
3497 */
3498 "}
3499 .replace("s", " ") // s is used as space placeholder to prevent format on save
3500 .as_str(),
3501 );
3502 // Ensure that delimiter space is preserved when space is not
3503 // on existing delimiter.
3504 cx.set_state(indoc! {"
3505 /**
3506 *ˇ
3507 */
3508 "});
3509 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3510 cx.assert_editor_state(indoc! {"
3511 /**
3512 *
3513 * ˇ
3514 */
3515 "});
3516 // Ensure that if suffix exists on same line after cursor it
3517 // doesn't add extra new line if prefix is not on same line.
3518 cx.set_state(indoc! {"
3519 /**
3520 ˇ*/
3521 "});
3522 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3523 cx.assert_editor_state(indoc! {"
3524 /**
3525
3526 ˇ*/
3527 "});
3528 // Ensure that it detects suffix after existing prefix.
3529 cx.set_state(indoc! {"
3530 /**ˇ/
3531 "});
3532 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3533 cx.assert_editor_state(indoc! {"
3534 /**
3535 ˇ/
3536 "});
3537 // Ensure that if suffix exists on same line before
3538 // cursor it does not add comment prefix.
3539 cx.set_state(indoc! {"
3540 /** */ˇ
3541 "});
3542 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3543 cx.assert_editor_state(indoc! {"
3544 /** */
3545 ˇ
3546 "});
3547 // Ensure that if suffix exists on same line before
3548 // cursor it does not add comment prefix.
3549 cx.set_state(indoc! {"
3550 /**
3551 *
3552 */ˇ
3553 "});
3554 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3555 cx.assert_editor_state(indoc! {"
3556 /**
3557 *
3558 */
3559 ˇ
3560 "});
3561
3562 // Ensure that inline comment followed by code
3563 // doesn't add comment prefix on newline
3564 cx.set_state(indoc! {"
3565 /** */ textˇ
3566 "});
3567 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3568 cx.assert_editor_state(indoc! {"
3569 /** */ text
3570 ˇ
3571 "});
3572
3573 // Ensure that text after comment end tag
3574 // doesn't add comment prefix on newline
3575 cx.set_state(indoc! {"
3576 /**
3577 *
3578 */ˇtext
3579 "});
3580 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3581 cx.assert_editor_state(indoc! {"
3582 /**
3583 *
3584 */
3585 ˇtext
3586 "});
3587
3588 // Ensure if not comment block it doesn't
3589 // add comment prefix on newline
3590 cx.set_state(indoc! {"
3591 * textˇ
3592 "});
3593 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3594 cx.assert_editor_state(indoc! {"
3595 * text
3596 ˇ
3597 "});
3598 }
3599 // Ensure that comment continuations can be disabled.
3600 update_test_language_settings(cx, |settings| {
3601 settings.defaults.extend_comment_on_newline = Some(false);
3602 });
3603 let mut cx = EditorTestContext::new(cx).await;
3604 cx.set_state(indoc! {"
3605 /**ˇ
3606 "});
3607 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3608 cx.assert_editor_state(indoc! {"
3609 /**
3610 ˇ
3611 "});
3612}
3613
3614#[gpui::test]
3615async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3616 init_test(cx, |settings| {
3617 settings.defaults.tab_size = NonZeroU32::new(4)
3618 });
3619
3620 let lua_language = Arc::new(Language::new(
3621 LanguageConfig {
3622 line_comments: vec!["--".into()],
3623 block_comment: Some(language::BlockCommentConfig {
3624 start: "--[[".into(),
3625 prefix: "".into(),
3626 end: "]]".into(),
3627 tab_size: 0,
3628 }),
3629 ..LanguageConfig::default()
3630 },
3631 None,
3632 ));
3633
3634 let mut cx = EditorTestContext::new(cx).await;
3635 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3636
3637 // Line with line comment should extend
3638 cx.set_state(indoc! {"
3639 --ˇ
3640 "});
3641 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3642 cx.assert_editor_state(indoc! {"
3643 --
3644 --ˇ
3645 "});
3646
3647 // Line with block comment that matches line comment should not extend
3648 cx.set_state(indoc! {"
3649 --[[ˇ
3650 "});
3651 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3652 cx.assert_editor_state(indoc! {"
3653 --[[
3654 ˇ
3655 "});
3656}
3657
3658#[gpui::test]
3659fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3660 init_test(cx, |_| {});
3661
3662 let editor = cx.add_window(|window, cx| {
3663 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3664 let mut editor = build_editor(buffer, window, cx);
3665 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3666 s.select_ranges([3..4, 11..12, 19..20])
3667 });
3668 editor
3669 });
3670
3671 _ = editor.update(cx, |editor, window, cx| {
3672 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3673 editor.buffer.update(cx, |buffer, cx| {
3674 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3675 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3676 });
3677 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3678
3679 editor.insert("Z", window, cx);
3680 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3681
3682 // The selections are moved after the inserted characters
3683 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3684 });
3685}
3686
3687#[gpui::test]
3688async fn test_tab(cx: &mut TestAppContext) {
3689 init_test(cx, |settings| {
3690 settings.defaults.tab_size = NonZeroU32::new(3)
3691 });
3692
3693 let mut cx = EditorTestContext::new(cx).await;
3694 cx.set_state(indoc! {"
3695 ˇabˇc
3696 ˇ🏀ˇ🏀ˇefg
3697 dˇ
3698 "});
3699 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3700 cx.assert_editor_state(indoc! {"
3701 ˇab ˇc
3702 ˇ🏀 ˇ🏀 ˇefg
3703 d ˇ
3704 "});
3705
3706 cx.set_state(indoc! {"
3707 a
3708 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3709 "});
3710 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3711 cx.assert_editor_state(indoc! {"
3712 a
3713 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3714 "});
3715}
3716
3717#[gpui::test]
3718async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3719 init_test(cx, |_| {});
3720
3721 let mut cx = EditorTestContext::new(cx).await;
3722 let language = Arc::new(
3723 Language::new(
3724 LanguageConfig::default(),
3725 Some(tree_sitter_rust::LANGUAGE.into()),
3726 )
3727 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3728 .unwrap(),
3729 );
3730 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3731
3732 // test when all cursors are not at suggested indent
3733 // then simply move to their suggested indent location
3734 cx.set_state(indoc! {"
3735 const a: B = (
3736 c(
3737 ˇ
3738 ˇ )
3739 );
3740 "});
3741 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3742 cx.assert_editor_state(indoc! {"
3743 const a: B = (
3744 c(
3745 ˇ
3746 ˇ)
3747 );
3748 "});
3749
3750 // test cursor already at suggested indent not moving when
3751 // other cursors are yet to reach their suggested indents
3752 cx.set_state(indoc! {"
3753 ˇ
3754 const a: B = (
3755 c(
3756 d(
3757 ˇ
3758 )
3759 ˇ
3760 ˇ )
3761 );
3762 "});
3763 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3764 cx.assert_editor_state(indoc! {"
3765 ˇ
3766 const a: B = (
3767 c(
3768 d(
3769 ˇ
3770 )
3771 ˇ
3772 ˇ)
3773 );
3774 "});
3775 // test when all cursors are at suggested indent then tab is inserted
3776 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3777 cx.assert_editor_state(indoc! {"
3778 ˇ
3779 const a: B = (
3780 c(
3781 d(
3782 ˇ
3783 )
3784 ˇ
3785 ˇ)
3786 );
3787 "});
3788
3789 // test when current indent is less than suggested indent,
3790 // we adjust line to match suggested indent and move cursor to it
3791 //
3792 // when no other cursor is at word boundary, all of them should move
3793 cx.set_state(indoc! {"
3794 const a: B = (
3795 c(
3796 d(
3797 ˇ
3798 ˇ )
3799 ˇ )
3800 );
3801 "});
3802 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3803 cx.assert_editor_state(indoc! {"
3804 const a: B = (
3805 c(
3806 d(
3807 ˇ
3808 ˇ)
3809 ˇ)
3810 );
3811 "});
3812
3813 // test when current indent is less than suggested indent,
3814 // we adjust line to match suggested indent and move cursor to it
3815 //
3816 // when some other cursor is at word boundary, it should not move
3817 cx.set_state(indoc! {"
3818 const a: B = (
3819 c(
3820 d(
3821 ˇ
3822 ˇ )
3823 ˇ)
3824 );
3825 "});
3826 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3827 cx.assert_editor_state(indoc! {"
3828 const a: B = (
3829 c(
3830 d(
3831 ˇ
3832 ˇ)
3833 ˇ)
3834 );
3835 "});
3836
3837 // test when current indent is more than suggested indent,
3838 // we just move cursor to current indent instead of suggested indent
3839 //
3840 // when no other cursor is at word boundary, all of them should move
3841 cx.set_state(indoc! {"
3842 const a: B = (
3843 c(
3844 d(
3845 ˇ
3846 ˇ )
3847 ˇ )
3848 );
3849 "});
3850 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3851 cx.assert_editor_state(indoc! {"
3852 const a: B = (
3853 c(
3854 d(
3855 ˇ
3856 ˇ)
3857 ˇ)
3858 );
3859 "});
3860 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3861 cx.assert_editor_state(indoc! {"
3862 const a: B = (
3863 c(
3864 d(
3865 ˇ
3866 ˇ)
3867 ˇ)
3868 );
3869 "});
3870
3871 // test when current indent is more than suggested indent,
3872 // we just move cursor to current indent instead of suggested indent
3873 //
3874 // when some other cursor is at word boundary, it doesn't move
3875 cx.set_state(indoc! {"
3876 const a: B = (
3877 c(
3878 d(
3879 ˇ
3880 ˇ )
3881 ˇ)
3882 );
3883 "});
3884 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3885 cx.assert_editor_state(indoc! {"
3886 const a: B = (
3887 c(
3888 d(
3889 ˇ
3890 ˇ)
3891 ˇ)
3892 );
3893 "});
3894
3895 // handle auto-indent when there are multiple cursors on the same line
3896 cx.set_state(indoc! {"
3897 const a: B = (
3898 c(
3899 ˇ ˇ
3900 ˇ )
3901 );
3902 "});
3903 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3904 cx.assert_editor_state(indoc! {"
3905 const a: B = (
3906 c(
3907 ˇ
3908 ˇ)
3909 );
3910 "});
3911}
3912
3913#[gpui::test]
3914async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3915 init_test(cx, |settings| {
3916 settings.defaults.tab_size = NonZeroU32::new(3)
3917 });
3918
3919 let mut cx = EditorTestContext::new(cx).await;
3920 cx.set_state(indoc! {"
3921 ˇ
3922 \t ˇ
3923 \t ˇ
3924 \t ˇ
3925 \t \t\t \t \t\t \t\t \t \t ˇ
3926 "});
3927
3928 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3929 cx.assert_editor_state(indoc! {"
3930 ˇ
3931 \t ˇ
3932 \t ˇ
3933 \t ˇ
3934 \t \t\t \t \t\t \t\t \t \t ˇ
3935 "});
3936}
3937
3938#[gpui::test]
3939async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3940 init_test(cx, |settings| {
3941 settings.defaults.tab_size = NonZeroU32::new(4)
3942 });
3943
3944 let language = Arc::new(
3945 Language::new(
3946 LanguageConfig::default(),
3947 Some(tree_sitter_rust::LANGUAGE.into()),
3948 )
3949 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3950 .unwrap(),
3951 );
3952
3953 let mut cx = EditorTestContext::new(cx).await;
3954 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3955 cx.set_state(indoc! {"
3956 fn a() {
3957 if b {
3958 \t ˇc
3959 }
3960 }
3961 "});
3962
3963 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3964 cx.assert_editor_state(indoc! {"
3965 fn a() {
3966 if b {
3967 ˇc
3968 }
3969 }
3970 "});
3971}
3972
3973#[gpui::test]
3974async fn test_indent_outdent(cx: &mut TestAppContext) {
3975 init_test(cx, |settings| {
3976 settings.defaults.tab_size = NonZeroU32::new(4);
3977 });
3978
3979 let mut cx = EditorTestContext::new(cx).await;
3980
3981 cx.set_state(indoc! {"
3982 «oneˇ» «twoˇ»
3983 three
3984 four
3985 "});
3986 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3987 cx.assert_editor_state(indoc! {"
3988 «oneˇ» «twoˇ»
3989 three
3990 four
3991 "});
3992
3993 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3994 cx.assert_editor_state(indoc! {"
3995 «oneˇ» «twoˇ»
3996 three
3997 four
3998 "});
3999
4000 // select across line ending
4001 cx.set_state(indoc! {"
4002 one two
4003 t«hree
4004 ˇ» four
4005 "});
4006 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4007 cx.assert_editor_state(indoc! {"
4008 one two
4009 t«hree
4010 ˇ» four
4011 "});
4012
4013 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4014 cx.assert_editor_state(indoc! {"
4015 one two
4016 t«hree
4017 ˇ» four
4018 "});
4019
4020 // Ensure that indenting/outdenting works when the cursor is at column 0.
4021 cx.set_state(indoc! {"
4022 one two
4023 ˇthree
4024 four
4025 "});
4026 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4027 cx.assert_editor_state(indoc! {"
4028 one two
4029 ˇthree
4030 four
4031 "});
4032
4033 cx.set_state(indoc! {"
4034 one two
4035 ˇ three
4036 four
4037 "});
4038 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4039 cx.assert_editor_state(indoc! {"
4040 one two
4041 ˇthree
4042 four
4043 "});
4044}
4045
4046#[gpui::test]
4047async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4048 // This is a regression test for issue #33761
4049 init_test(cx, |_| {});
4050
4051 let mut cx = EditorTestContext::new(cx).await;
4052 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4053 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4054
4055 cx.set_state(
4056 r#"ˇ# ingress:
4057ˇ# api:
4058ˇ# enabled: false
4059ˇ# pathType: Prefix
4060ˇ# console:
4061ˇ# enabled: false
4062ˇ# pathType: Prefix
4063"#,
4064 );
4065
4066 // Press tab to indent all lines
4067 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4068
4069 cx.assert_editor_state(
4070 r#" ˇ# ingress:
4071 ˇ# api:
4072 ˇ# enabled: false
4073 ˇ# pathType: Prefix
4074 ˇ# console:
4075 ˇ# enabled: false
4076 ˇ# pathType: Prefix
4077"#,
4078 );
4079}
4080
4081#[gpui::test]
4082async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4083 // This is a test to make sure our fix for issue #33761 didn't break anything
4084 init_test(cx, |_| {});
4085
4086 let mut cx = EditorTestContext::new(cx).await;
4087 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4088 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4089
4090 cx.set_state(
4091 r#"ˇingress:
4092ˇ api:
4093ˇ enabled: false
4094ˇ pathType: Prefix
4095"#,
4096 );
4097
4098 // Press tab to indent all lines
4099 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4100
4101 cx.assert_editor_state(
4102 r#"ˇingress:
4103 ˇapi:
4104 ˇenabled: false
4105 ˇpathType: Prefix
4106"#,
4107 );
4108}
4109
4110#[gpui::test]
4111async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4112 init_test(cx, |settings| {
4113 settings.defaults.hard_tabs = Some(true);
4114 });
4115
4116 let mut cx = EditorTestContext::new(cx).await;
4117
4118 // select two ranges on one line
4119 cx.set_state(indoc! {"
4120 «oneˇ» «twoˇ»
4121 three
4122 four
4123 "});
4124 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4125 cx.assert_editor_state(indoc! {"
4126 \t«oneˇ» «twoˇ»
4127 three
4128 four
4129 "});
4130 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4131 cx.assert_editor_state(indoc! {"
4132 \t\t«oneˇ» «twoˇ»
4133 three
4134 four
4135 "});
4136 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4137 cx.assert_editor_state(indoc! {"
4138 \t«oneˇ» «twoˇ»
4139 three
4140 four
4141 "});
4142 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4143 cx.assert_editor_state(indoc! {"
4144 «oneˇ» «twoˇ»
4145 three
4146 four
4147 "});
4148
4149 // select across a line ending
4150 cx.set_state(indoc! {"
4151 one two
4152 t«hree
4153 ˇ»four
4154 "});
4155 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4156 cx.assert_editor_state(indoc! {"
4157 one two
4158 \tt«hree
4159 ˇ»four
4160 "});
4161 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4162 cx.assert_editor_state(indoc! {"
4163 one two
4164 \t\tt«hree
4165 ˇ»four
4166 "});
4167 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4168 cx.assert_editor_state(indoc! {"
4169 one two
4170 \tt«hree
4171 ˇ»four
4172 "});
4173 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4174 cx.assert_editor_state(indoc! {"
4175 one two
4176 t«hree
4177 ˇ»four
4178 "});
4179
4180 // Ensure that indenting/outdenting works when the cursor is at column 0.
4181 cx.set_state(indoc! {"
4182 one two
4183 ˇthree
4184 four
4185 "});
4186 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4187 cx.assert_editor_state(indoc! {"
4188 one two
4189 ˇthree
4190 four
4191 "});
4192 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4193 cx.assert_editor_state(indoc! {"
4194 one two
4195 \tˇthree
4196 four
4197 "});
4198 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4199 cx.assert_editor_state(indoc! {"
4200 one two
4201 ˇthree
4202 four
4203 "});
4204}
4205
4206#[gpui::test]
4207fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4208 init_test(cx, |settings| {
4209 settings.languages.0.extend([
4210 (
4211 "TOML".into(),
4212 LanguageSettingsContent {
4213 tab_size: NonZeroU32::new(2),
4214 ..Default::default()
4215 },
4216 ),
4217 (
4218 "Rust".into(),
4219 LanguageSettingsContent {
4220 tab_size: NonZeroU32::new(4),
4221 ..Default::default()
4222 },
4223 ),
4224 ]);
4225 });
4226
4227 let toml_language = Arc::new(Language::new(
4228 LanguageConfig {
4229 name: "TOML".into(),
4230 ..Default::default()
4231 },
4232 None,
4233 ));
4234 let rust_language = Arc::new(Language::new(
4235 LanguageConfig {
4236 name: "Rust".into(),
4237 ..Default::default()
4238 },
4239 None,
4240 ));
4241
4242 let toml_buffer =
4243 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4244 let rust_buffer =
4245 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4246 let multibuffer = cx.new(|cx| {
4247 let mut multibuffer = MultiBuffer::new(ReadWrite);
4248 multibuffer.push_excerpts(
4249 toml_buffer.clone(),
4250 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4251 cx,
4252 );
4253 multibuffer.push_excerpts(
4254 rust_buffer.clone(),
4255 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4256 cx,
4257 );
4258 multibuffer
4259 });
4260
4261 cx.add_window(|window, cx| {
4262 let mut editor = build_editor(multibuffer, window, cx);
4263
4264 assert_eq!(
4265 editor.text(cx),
4266 indoc! {"
4267 a = 1
4268 b = 2
4269
4270 const c: usize = 3;
4271 "}
4272 );
4273
4274 select_ranges(
4275 &mut editor,
4276 indoc! {"
4277 «aˇ» = 1
4278 b = 2
4279
4280 «const c:ˇ» usize = 3;
4281 "},
4282 window,
4283 cx,
4284 );
4285
4286 editor.tab(&Tab, window, cx);
4287 assert_text_with_selections(
4288 &mut editor,
4289 indoc! {"
4290 «aˇ» = 1
4291 b = 2
4292
4293 «const c:ˇ» usize = 3;
4294 "},
4295 cx,
4296 );
4297 editor.backtab(&Backtab, window, cx);
4298 assert_text_with_selections(
4299 &mut editor,
4300 indoc! {"
4301 «aˇ» = 1
4302 b = 2
4303
4304 «const c:ˇ» usize = 3;
4305 "},
4306 cx,
4307 );
4308
4309 editor
4310 });
4311}
4312
4313#[gpui::test]
4314async fn test_backspace(cx: &mut TestAppContext) {
4315 init_test(cx, |_| {});
4316
4317 let mut cx = EditorTestContext::new(cx).await;
4318
4319 // Basic backspace
4320 cx.set_state(indoc! {"
4321 onˇe two three
4322 fou«rˇ» five six
4323 seven «ˇeight nine
4324 »ten
4325 "});
4326 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4327 cx.assert_editor_state(indoc! {"
4328 oˇe two three
4329 fouˇ five six
4330 seven ˇten
4331 "});
4332
4333 // Test backspace inside and around indents
4334 cx.set_state(indoc! {"
4335 zero
4336 ˇone
4337 ˇtwo
4338 ˇ ˇ ˇ three
4339 ˇ ˇ four
4340 "});
4341 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4342 cx.assert_editor_state(indoc! {"
4343 zero
4344 ˇone
4345 ˇtwo
4346 ˇ threeˇ four
4347 "});
4348}
4349
4350#[gpui::test]
4351async fn test_delete(cx: &mut TestAppContext) {
4352 init_test(cx, |_| {});
4353
4354 let mut cx = EditorTestContext::new(cx).await;
4355 cx.set_state(indoc! {"
4356 onˇe two three
4357 fou«rˇ» five six
4358 seven «ˇeight nine
4359 »ten
4360 "});
4361 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4362 cx.assert_editor_state(indoc! {"
4363 onˇ two three
4364 fouˇ five six
4365 seven ˇten
4366 "});
4367}
4368
4369#[gpui::test]
4370fn test_delete_line(cx: &mut TestAppContext) {
4371 init_test(cx, |_| {});
4372
4373 let editor = cx.add_window(|window, cx| {
4374 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4375 build_editor(buffer, window, cx)
4376 });
4377 _ = editor.update(cx, |editor, window, cx| {
4378 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4379 s.select_display_ranges([
4380 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4381 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4382 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4383 ])
4384 });
4385 editor.delete_line(&DeleteLine, window, cx);
4386 assert_eq!(editor.display_text(cx), "ghi");
4387 assert_eq!(
4388 editor.selections.display_ranges(cx),
4389 vec![
4390 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4391 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
4392 ]
4393 );
4394 });
4395
4396 let editor = cx.add_window(|window, cx| {
4397 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4398 build_editor(buffer, window, cx)
4399 });
4400 _ = editor.update(cx, |editor, window, cx| {
4401 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4402 s.select_display_ranges([
4403 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4404 ])
4405 });
4406 editor.delete_line(&DeleteLine, window, cx);
4407 assert_eq!(editor.display_text(cx), "ghi\n");
4408 assert_eq!(
4409 editor.selections.display_ranges(cx),
4410 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4411 );
4412 });
4413}
4414
4415#[gpui::test]
4416fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4417 init_test(cx, |_| {});
4418
4419 cx.add_window(|window, cx| {
4420 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4421 let mut editor = build_editor(buffer.clone(), window, cx);
4422 let buffer = buffer.read(cx).as_singleton().unwrap();
4423
4424 assert_eq!(
4425 editor.selections.ranges::<Point>(cx),
4426 &[Point::new(0, 0)..Point::new(0, 0)]
4427 );
4428
4429 // When on single line, replace newline at end by space
4430 editor.join_lines(&JoinLines, window, cx);
4431 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4432 assert_eq!(
4433 editor.selections.ranges::<Point>(cx),
4434 &[Point::new(0, 3)..Point::new(0, 3)]
4435 );
4436
4437 // When multiple lines are selected, remove newlines that are spanned by the selection
4438 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4439 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4440 });
4441 editor.join_lines(&JoinLines, window, cx);
4442 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4443 assert_eq!(
4444 editor.selections.ranges::<Point>(cx),
4445 &[Point::new(0, 11)..Point::new(0, 11)]
4446 );
4447
4448 // Undo should be transactional
4449 editor.undo(&Undo, window, cx);
4450 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4451 assert_eq!(
4452 editor.selections.ranges::<Point>(cx),
4453 &[Point::new(0, 5)..Point::new(2, 2)]
4454 );
4455
4456 // When joining an empty line don't insert a space
4457 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4458 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4459 });
4460 editor.join_lines(&JoinLines, window, cx);
4461 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4462 assert_eq!(
4463 editor.selections.ranges::<Point>(cx),
4464 [Point::new(2, 3)..Point::new(2, 3)]
4465 );
4466
4467 // We can remove trailing newlines
4468 editor.join_lines(&JoinLines, window, cx);
4469 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4470 assert_eq!(
4471 editor.selections.ranges::<Point>(cx),
4472 [Point::new(2, 3)..Point::new(2, 3)]
4473 );
4474
4475 // We don't blow up on the last line
4476 editor.join_lines(&JoinLines, window, cx);
4477 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4478 assert_eq!(
4479 editor.selections.ranges::<Point>(cx),
4480 [Point::new(2, 3)..Point::new(2, 3)]
4481 );
4482
4483 // reset to test indentation
4484 editor.buffer.update(cx, |buffer, cx| {
4485 buffer.edit(
4486 [
4487 (Point::new(1, 0)..Point::new(1, 2), " "),
4488 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4489 ],
4490 None,
4491 cx,
4492 )
4493 });
4494
4495 // We remove any leading spaces
4496 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4497 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4498 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4499 });
4500 editor.join_lines(&JoinLines, window, cx);
4501 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4502
4503 // We don't insert a space for a line containing only spaces
4504 editor.join_lines(&JoinLines, window, cx);
4505 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4506
4507 // We ignore any leading tabs
4508 editor.join_lines(&JoinLines, window, cx);
4509 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4510
4511 editor
4512 });
4513}
4514
4515#[gpui::test]
4516fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4517 init_test(cx, |_| {});
4518
4519 cx.add_window(|window, cx| {
4520 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4521 let mut editor = build_editor(buffer.clone(), window, cx);
4522 let buffer = buffer.read(cx).as_singleton().unwrap();
4523
4524 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4525 s.select_ranges([
4526 Point::new(0, 2)..Point::new(1, 1),
4527 Point::new(1, 2)..Point::new(1, 2),
4528 Point::new(3, 1)..Point::new(3, 2),
4529 ])
4530 });
4531
4532 editor.join_lines(&JoinLines, window, cx);
4533 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4534
4535 assert_eq!(
4536 editor.selections.ranges::<Point>(cx),
4537 [
4538 Point::new(0, 7)..Point::new(0, 7),
4539 Point::new(1, 3)..Point::new(1, 3)
4540 ]
4541 );
4542 editor
4543 });
4544}
4545
4546#[gpui::test]
4547async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4548 init_test(cx, |_| {});
4549
4550 let mut cx = EditorTestContext::new(cx).await;
4551
4552 let diff_base = r#"
4553 Line 0
4554 Line 1
4555 Line 2
4556 Line 3
4557 "#
4558 .unindent();
4559
4560 cx.set_state(
4561 &r#"
4562 ˇLine 0
4563 Line 1
4564 Line 2
4565 Line 3
4566 "#
4567 .unindent(),
4568 );
4569
4570 cx.set_head_text(&diff_base);
4571 executor.run_until_parked();
4572
4573 // Join lines
4574 cx.update_editor(|editor, window, cx| {
4575 editor.join_lines(&JoinLines, window, cx);
4576 });
4577 executor.run_until_parked();
4578
4579 cx.assert_editor_state(
4580 &r#"
4581 Line 0ˇ Line 1
4582 Line 2
4583 Line 3
4584 "#
4585 .unindent(),
4586 );
4587 // Join again
4588 cx.update_editor(|editor, window, cx| {
4589 editor.join_lines(&JoinLines, window, cx);
4590 });
4591 executor.run_until_parked();
4592
4593 cx.assert_editor_state(
4594 &r#"
4595 Line 0 Line 1ˇ Line 2
4596 Line 3
4597 "#
4598 .unindent(),
4599 );
4600}
4601
4602#[gpui::test]
4603async fn test_custom_newlines_cause_no_false_positive_diffs(
4604 executor: BackgroundExecutor,
4605 cx: &mut TestAppContext,
4606) {
4607 init_test(cx, |_| {});
4608 let mut cx = EditorTestContext::new(cx).await;
4609 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4610 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4611 executor.run_until_parked();
4612
4613 cx.update_editor(|editor, window, cx| {
4614 let snapshot = editor.snapshot(window, cx);
4615 assert_eq!(
4616 snapshot
4617 .buffer_snapshot()
4618 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
4619 .collect::<Vec<_>>(),
4620 Vec::new(),
4621 "Should not have any diffs for files with custom newlines"
4622 );
4623 });
4624}
4625
4626#[gpui::test]
4627async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4628 init_test(cx, |_| {});
4629
4630 let mut cx = EditorTestContext::new(cx).await;
4631
4632 // Test sort_lines_case_insensitive()
4633 cx.set_state(indoc! {"
4634 «z
4635 y
4636 x
4637 Z
4638 Y
4639 Xˇ»
4640 "});
4641 cx.update_editor(|e, window, cx| {
4642 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4643 });
4644 cx.assert_editor_state(indoc! {"
4645 «x
4646 X
4647 y
4648 Y
4649 z
4650 Zˇ»
4651 "});
4652
4653 // Test sort_lines_by_length()
4654 //
4655 // Demonstrates:
4656 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4657 // - sort is stable
4658 cx.set_state(indoc! {"
4659 «123
4660 æ
4661 12
4662 ∞
4663 1
4664 æˇ»
4665 "});
4666 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4667 cx.assert_editor_state(indoc! {"
4668 «æ
4669 ∞
4670 1
4671 æ
4672 12
4673 123ˇ»
4674 "});
4675
4676 // Test reverse_lines()
4677 cx.set_state(indoc! {"
4678 «5
4679 4
4680 3
4681 2
4682 1ˇ»
4683 "});
4684 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4685 cx.assert_editor_state(indoc! {"
4686 «1
4687 2
4688 3
4689 4
4690 5ˇ»
4691 "});
4692
4693 // Skip testing shuffle_line()
4694
4695 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4696 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4697
4698 // Don't manipulate when cursor is on single line, but expand the selection
4699 cx.set_state(indoc! {"
4700 ddˇdd
4701 ccc
4702 bb
4703 a
4704 "});
4705 cx.update_editor(|e, window, cx| {
4706 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4707 });
4708 cx.assert_editor_state(indoc! {"
4709 «ddddˇ»
4710 ccc
4711 bb
4712 a
4713 "});
4714
4715 // Basic manipulate case
4716 // Start selection moves to column 0
4717 // End of selection shrinks to fit shorter line
4718 cx.set_state(indoc! {"
4719 dd«d
4720 ccc
4721 bb
4722 aaaaaˇ»
4723 "});
4724 cx.update_editor(|e, window, cx| {
4725 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4726 });
4727 cx.assert_editor_state(indoc! {"
4728 «aaaaa
4729 bb
4730 ccc
4731 dddˇ»
4732 "});
4733
4734 // Manipulate case with newlines
4735 cx.set_state(indoc! {"
4736 dd«d
4737 ccc
4738
4739 bb
4740 aaaaa
4741
4742 ˇ»
4743 "});
4744 cx.update_editor(|e, window, cx| {
4745 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4746 });
4747 cx.assert_editor_state(indoc! {"
4748 «
4749
4750 aaaaa
4751 bb
4752 ccc
4753 dddˇ»
4754
4755 "});
4756
4757 // Adding new line
4758 cx.set_state(indoc! {"
4759 aa«a
4760 bbˇ»b
4761 "});
4762 cx.update_editor(|e, window, cx| {
4763 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4764 });
4765 cx.assert_editor_state(indoc! {"
4766 «aaa
4767 bbb
4768 added_lineˇ»
4769 "});
4770
4771 // Removing line
4772 cx.set_state(indoc! {"
4773 aa«a
4774 bbbˇ»
4775 "});
4776 cx.update_editor(|e, window, cx| {
4777 e.manipulate_immutable_lines(window, cx, |lines| {
4778 lines.pop();
4779 })
4780 });
4781 cx.assert_editor_state(indoc! {"
4782 «aaaˇ»
4783 "});
4784
4785 // Removing all lines
4786 cx.set_state(indoc! {"
4787 aa«a
4788 bbbˇ»
4789 "});
4790 cx.update_editor(|e, window, cx| {
4791 e.manipulate_immutable_lines(window, cx, |lines| {
4792 lines.drain(..);
4793 })
4794 });
4795 cx.assert_editor_state(indoc! {"
4796 ˇ
4797 "});
4798}
4799
4800#[gpui::test]
4801async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4802 init_test(cx, |_| {});
4803
4804 let mut cx = EditorTestContext::new(cx).await;
4805
4806 // Consider continuous selection as single selection
4807 cx.set_state(indoc! {"
4808 Aaa«aa
4809 cˇ»c«c
4810 bb
4811 aaaˇ»aa
4812 "});
4813 cx.update_editor(|e, window, cx| {
4814 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4815 });
4816 cx.assert_editor_state(indoc! {"
4817 «Aaaaa
4818 ccc
4819 bb
4820 aaaaaˇ»
4821 "});
4822
4823 cx.set_state(indoc! {"
4824 Aaa«aa
4825 cˇ»c«c
4826 bb
4827 aaaˇ»aa
4828 "});
4829 cx.update_editor(|e, window, cx| {
4830 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4831 });
4832 cx.assert_editor_state(indoc! {"
4833 «Aaaaa
4834 ccc
4835 bbˇ»
4836 "});
4837
4838 // Consider non continuous selection as distinct dedup operations
4839 cx.set_state(indoc! {"
4840 «aaaaa
4841 bb
4842 aaaaa
4843 aaaaaˇ»
4844
4845 aaa«aaˇ»
4846 "});
4847 cx.update_editor(|e, window, cx| {
4848 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4849 });
4850 cx.assert_editor_state(indoc! {"
4851 «aaaaa
4852 bbˇ»
4853
4854 «aaaaaˇ»
4855 "});
4856}
4857
4858#[gpui::test]
4859async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4860 init_test(cx, |_| {});
4861
4862 let mut cx = EditorTestContext::new(cx).await;
4863
4864 cx.set_state(indoc! {"
4865 «Aaa
4866 aAa
4867 Aaaˇ»
4868 "});
4869 cx.update_editor(|e, window, cx| {
4870 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4871 });
4872 cx.assert_editor_state(indoc! {"
4873 «Aaa
4874 aAaˇ»
4875 "});
4876
4877 cx.set_state(indoc! {"
4878 «Aaa
4879 aAa
4880 aaAˇ»
4881 "});
4882 cx.update_editor(|e, window, cx| {
4883 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4884 });
4885 cx.assert_editor_state(indoc! {"
4886 «Aaaˇ»
4887 "});
4888}
4889
4890#[gpui::test]
4891async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4892 init_test(cx, |_| {});
4893
4894 let mut cx = EditorTestContext::new(cx).await;
4895
4896 let js_language = Arc::new(Language::new(
4897 LanguageConfig {
4898 name: "JavaScript".into(),
4899 wrap_characters: Some(language::WrapCharactersConfig {
4900 start_prefix: "<".into(),
4901 start_suffix: ">".into(),
4902 end_prefix: "</".into(),
4903 end_suffix: ">".into(),
4904 }),
4905 ..LanguageConfig::default()
4906 },
4907 None,
4908 ));
4909
4910 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4911
4912 cx.set_state(indoc! {"
4913 «testˇ»
4914 "});
4915 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4916 cx.assert_editor_state(indoc! {"
4917 <«ˇ»>test</«ˇ»>
4918 "});
4919
4920 cx.set_state(indoc! {"
4921 «test
4922 testˇ»
4923 "});
4924 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4925 cx.assert_editor_state(indoc! {"
4926 <«ˇ»>test
4927 test</«ˇ»>
4928 "});
4929
4930 cx.set_state(indoc! {"
4931 teˇst
4932 "});
4933 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4934 cx.assert_editor_state(indoc! {"
4935 te<«ˇ»></«ˇ»>st
4936 "});
4937}
4938
4939#[gpui::test]
4940async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
4941 init_test(cx, |_| {});
4942
4943 let mut cx = EditorTestContext::new(cx).await;
4944
4945 let js_language = Arc::new(Language::new(
4946 LanguageConfig {
4947 name: "JavaScript".into(),
4948 wrap_characters: Some(language::WrapCharactersConfig {
4949 start_prefix: "<".into(),
4950 start_suffix: ">".into(),
4951 end_prefix: "</".into(),
4952 end_suffix: ">".into(),
4953 }),
4954 ..LanguageConfig::default()
4955 },
4956 None,
4957 ));
4958
4959 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4960
4961 cx.set_state(indoc! {"
4962 «testˇ»
4963 «testˇ» «testˇ»
4964 «testˇ»
4965 "});
4966 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4967 cx.assert_editor_state(indoc! {"
4968 <«ˇ»>test</«ˇ»>
4969 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
4970 <«ˇ»>test</«ˇ»>
4971 "});
4972
4973 cx.set_state(indoc! {"
4974 «test
4975 testˇ»
4976 «test
4977 testˇ»
4978 "});
4979 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4980 cx.assert_editor_state(indoc! {"
4981 <«ˇ»>test
4982 test</«ˇ»>
4983 <«ˇ»>test
4984 test</«ˇ»>
4985 "});
4986}
4987
4988#[gpui::test]
4989async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
4990 init_test(cx, |_| {});
4991
4992 let mut cx = EditorTestContext::new(cx).await;
4993
4994 let plaintext_language = Arc::new(Language::new(
4995 LanguageConfig {
4996 name: "Plain Text".into(),
4997 ..LanguageConfig::default()
4998 },
4999 None,
5000 ));
5001
5002 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5003
5004 cx.set_state(indoc! {"
5005 «testˇ»
5006 "});
5007 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5008 cx.assert_editor_state(indoc! {"
5009 «testˇ»
5010 "});
5011}
5012
5013#[gpui::test]
5014async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5015 init_test(cx, |_| {});
5016
5017 let mut cx = EditorTestContext::new(cx).await;
5018
5019 // Manipulate with multiple selections on a single line
5020 cx.set_state(indoc! {"
5021 dd«dd
5022 cˇ»c«c
5023 bb
5024 aaaˇ»aa
5025 "});
5026 cx.update_editor(|e, window, cx| {
5027 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5028 });
5029 cx.assert_editor_state(indoc! {"
5030 «aaaaa
5031 bb
5032 ccc
5033 ddddˇ»
5034 "});
5035
5036 // Manipulate with multiple disjoin selections
5037 cx.set_state(indoc! {"
5038 5«
5039 4
5040 3
5041 2
5042 1ˇ»
5043
5044 dd«dd
5045 ccc
5046 bb
5047 aaaˇ»aa
5048 "});
5049 cx.update_editor(|e, window, cx| {
5050 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5051 });
5052 cx.assert_editor_state(indoc! {"
5053 «1
5054 2
5055 3
5056 4
5057 5ˇ»
5058
5059 «aaaaa
5060 bb
5061 ccc
5062 ddddˇ»
5063 "});
5064
5065 // Adding lines on each selection
5066 cx.set_state(indoc! {"
5067 2«
5068 1ˇ»
5069
5070 bb«bb
5071 aaaˇ»aa
5072 "});
5073 cx.update_editor(|e, window, cx| {
5074 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5075 });
5076 cx.assert_editor_state(indoc! {"
5077 «2
5078 1
5079 added lineˇ»
5080
5081 «bbbb
5082 aaaaa
5083 added lineˇ»
5084 "});
5085
5086 // Removing lines on each selection
5087 cx.set_state(indoc! {"
5088 2«
5089 1ˇ»
5090
5091 bb«bb
5092 aaaˇ»aa
5093 "});
5094 cx.update_editor(|e, window, cx| {
5095 e.manipulate_immutable_lines(window, cx, |lines| {
5096 lines.pop();
5097 })
5098 });
5099 cx.assert_editor_state(indoc! {"
5100 «2ˇ»
5101
5102 «bbbbˇ»
5103 "});
5104}
5105
5106#[gpui::test]
5107async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5108 init_test(cx, |settings| {
5109 settings.defaults.tab_size = NonZeroU32::new(3)
5110 });
5111
5112 let mut cx = EditorTestContext::new(cx).await;
5113
5114 // MULTI SELECTION
5115 // Ln.1 "«" tests empty lines
5116 // Ln.9 tests just leading whitespace
5117 cx.set_state(indoc! {"
5118 «
5119 abc // No indentationˇ»
5120 «\tabc // 1 tabˇ»
5121 \t\tabc « ˇ» // 2 tabs
5122 \t ab«c // Tab followed by space
5123 \tabc // Space followed by tab (3 spaces should be the result)
5124 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5125 abˇ»ˇc ˇ ˇ // Already space indented«
5126 \t
5127 \tabc\tdef // Only the leading tab is manipulatedˇ»
5128 "});
5129 cx.update_editor(|e, window, cx| {
5130 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5131 });
5132 cx.assert_editor_state(
5133 indoc! {"
5134 «
5135 abc // No indentation
5136 abc // 1 tab
5137 abc // 2 tabs
5138 abc // Tab followed by space
5139 abc // Space followed by tab (3 spaces should be the result)
5140 abc // Mixed indentation (tab conversion depends on the column)
5141 abc // Already space indented
5142 ·
5143 abc\tdef // Only the leading tab is manipulatedˇ»
5144 "}
5145 .replace("·", "")
5146 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5147 );
5148
5149 // Test on just a few lines, the others should remain unchanged
5150 // Only lines (3, 5, 10, 11) should change
5151 cx.set_state(
5152 indoc! {"
5153 ·
5154 abc // No indentation
5155 \tabcˇ // 1 tab
5156 \t\tabc // 2 tabs
5157 \t abcˇ // Tab followed by space
5158 \tabc // Space followed by tab (3 spaces should be the result)
5159 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5160 abc // Already space indented
5161 «\t
5162 \tabc\tdef // Only the leading tab is manipulatedˇ»
5163 "}
5164 .replace("·", "")
5165 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5166 );
5167 cx.update_editor(|e, window, cx| {
5168 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5169 });
5170 cx.assert_editor_state(
5171 indoc! {"
5172 ·
5173 abc // No indentation
5174 « abc // 1 tabˇ»
5175 \t\tabc // 2 tabs
5176 « abc // Tab followed by spaceˇ»
5177 \tabc // Space followed by tab (3 spaces should be the result)
5178 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5179 abc // Already space indented
5180 « ·
5181 abc\tdef // Only the leading tab is manipulatedˇ»
5182 "}
5183 .replace("·", "")
5184 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5185 );
5186
5187 // SINGLE SELECTION
5188 // Ln.1 "«" tests empty lines
5189 // Ln.9 tests just leading whitespace
5190 cx.set_state(indoc! {"
5191 «
5192 abc // No indentation
5193 \tabc // 1 tab
5194 \t\tabc // 2 tabs
5195 \t abc // Tab followed by space
5196 \tabc // Space followed by tab (3 spaces should be the result)
5197 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5198 abc // Already space indented
5199 \t
5200 \tabc\tdef // Only the leading tab is manipulatedˇ»
5201 "});
5202 cx.update_editor(|e, window, cx| {
5203 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5204 });
5205 cx.assert_editor_state(
5206 indoc! {"
5207 «
5208 abc // No indentation
5209 abc // 1 tab
5210 abc // 2 tabs
5211 abc // Tab followed by space
5212 abc // Space followed by tab (3 spaces should be the result)
5213 abc // Mixed indentation (tab conversion depends on the column)
5214 abc // Already space indented
5215 ·
5216 abc\tdef // Only the leading tab is manipulatedˇ»
5217 "}
5218 .replace("·", "")
5219 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5220 );
5221}
5222
5223#[gpui::test]
5224async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5225 init_test(cx, |settings| {
5226 settings.defaults.tab_size = NonZeroU32::new(3)
5227 });
5228
5229 let mut cx = EditorTestContext::new(cx).await;
5230
5231 // MULTI SELECTION
5232 // Ln.1 "«" tests empty lines
5233 // Ln.11 tests just leading whitespace
5234 cx.set_state(indoc! {"
5235 «
5236 abˇ»ˇc // No indentation
5237 abc ˇ ˇ // 1 space (< 3 so dont convert)
5238 abc « // 2 spaces (< 3 so dont convert)
5239 abc // 3 spaces (convert)
5240 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5241 «\tˇ»\t«\tˇ»abc // Already tab indented
5242 «\t abc // Tab followed by space
5243 \tabc // Space followed by tab (should be consumed due to tab)
5244 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5245 \tˇ» «\t
5246 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5247 "});
5248 cx.update_editor(|e, window, cx| {
5249 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5250 });
5251 cx.assert_editor_state(indoc! {"
5252 «
5253 abc // No indentation
5254 abc // 1 space (< 3 so dont convert)
5255 abc // 2 spaces (< 3 so dont convert)
5256 \tabc // 3 spaces (convert)
5257 \t abc // 5 spaces (1 tab + 2 spaces)
5258 \t\t\tabc // 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\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5262 \t\t\t
5263 \tabc \t // Only the leading spaces should be convertedˇ»
5264 "});
5265
5266 // Test on just a few lines, the other should remain unchanged
5267 // Only lines (4, 8, 11, 12) should change
5268 cx.set_state(
5269 indoc! {"
5270 ·
5271 abc // No indentation
5272 abc // 1 space (< 3 so dont convert)
5273 abc // 2 spaces (< 3 so dont convert)
5274 « abc // 3 spaces (convert)ˇ»
5275 abc // 5 spaces (1 tab + 2 spaces)
5276 \t\t\tabc // Already tab indented
5277 \t abc // Tab followed by space
5278 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5279 \t\t \tabc // Mixed indentation
5280 \t \t \t \tabc // Mixed indentation
5281 \t \tˇ
5282 « abc \t // Only the leading spaces should be convertedˇ»
5283 "}
5284 .replace("·", "")
5285 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5286 );
5287 cx.update_editor(|e, window, cx| {
5288 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5289 });
5290 cx.assert_editor_state(
5291 indoc! {"
5292 ·
5293 abc // No indentation
5294 abc // 1 space (< 3 so dont convert)
5295 abc // 2 spaces (< 3 so dont convert)
5296 «\tabc // 3 spaces (convert)ˇ»
5297 abc // 5 spaces (1 tab + 2 spaces)
5298 \t\t\tabc // Already tab indented
5299 \t abc // Tab followed by space
5300 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5301 \t\t \tabc // Mixed indentation
5302 \t \t \t \tabc // Mixed indentation
5303 «\t\t\t
5304 \tabc \t // Only the leading spaces should be convertedˇ»
5305 "}
5306 .replace("·", "")
5307 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5308 );
5309
5310 // SINGLE SELECTION
5311 // Ln.1 "«" tests empty lines
5312 // Ln.11 tests just leading whitespace
5313 cx.set_state(indoc! {"
5314 «
5315 abc // No indentation
5316 abc // 1 space (< 3 so dont convert)
5317 abc // 2 spaces (< 3 so dont convert)
5318 abc // 3 spaces (convert)
5319 abc // 5 spaces (1 tab + 2 spaces)
5320 \t\t\tabc // Already tab indented
5321 \t abc // Tab followed by space
5322 \tabc // Space followed by tab (should be consumed due to tab)
5323 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5324 \t \t
5325 abc \t // Only the leading spaces should be convertedˇ»
5326 "});
5327 cx.update_editor(|e, window, cx| {
5328 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5329 });
5330 cx.assert_editor_state(indoc! {"
5331 «
5332 abc // No indentation
5333 abc // 1 space (< 3 so dont convert)
5334 abc // 2 spaces (< 3 so dont convert)
5335 \tabc // 3 spaces (convert)
5336 \t 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\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5341 \t\t\t
5342 \tabc \t // Only the leading spaces should be convertedˇ»
5343 "});
5344}
5345
5346#[gpui::test]
5347async fn test_toggle_case(cx: &mut TestAppContext) {
5348 init_test(cx, |_| {});
5349
5350 let mut cx = EditorTestContext::new(cx).await;
5351
5352 // If all lower case -> upper case
5353 cx.set_state(indoc! {"
5354 «hello worldˇ»
5355 "});
5356 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5357 cx.assert_editor_state(indoc! {"
5358 «HELLO WORLDˇ»
5359 "});
5360
5361 // If all upper case -> lower case
5362 cx.set_state(indoc! {"
5363 «HELLO WORLDˇ»
5364 "});
5365 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5366 cx.assert_editor_state(indoc! {"
5367 «hello worldˇ»
5368 "});
5369
5370 // If any upper case characters are identified -> lower case
5371 // This matches JetBrains IDEs
5372 cx.set_state(indoc! {"
5373 «hEllo worldˇ»
5374 "});
5375 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5376 cx.assert_editor_state(indoc! {"
5377 «hello worldˇ»
5378 "});
5379}
5380
5381#[gpui::test]
5382async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5383 init_test(cx, |_| {});
5384
5385 let mut cx = EditorTestContext::new(cx).await;
5386
5387 cx.set_state(indoc! {"
5388 «implement-windows-supportˇ»
5389 "});
5390 cx.update_editor(|e, window, cx| {
5391 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5392 });
5393 cx.assert_editor_state(indoc! {"
5394 «Implement windows supportˇ»
5395 "});
5396}
5397
5398#[gpui::test]
5399async fn test_manipulate_text(cx: &mut TestAppContext) {
5400 init_test(cx, |_| {});
5401
5402 let mut cx = EditorTestContext::new(cx).await;
5403
5404 // Test convert_to_upper_case()
5405 cx.set_state(indoc! {"
5406 «hello worldˇ»
5407 "});
5408 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5409 cx.assert_editor_state(indoc! {"
5410 «HELLO WORLDˇ»
5411 "});
5412
5413 // Test convert_to_lower_case()
5414 cx.set_state(indoc! {"
5415 «HELLO WORLDˇ»
5416 "});
5417 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5418 cx.assert_editor_state(indoc! {"
5419 «hello worldˇ»
5420 "});
5421
5422 // Test multiple line, single selection case
5423 cx.set_state(indoc! {"
5424 «The quick brown
5425 fox jumps over
5426 the lazy dogˇ»
5427 "});
5428 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5429 cx.assert_editor_state(indoc! {"
5430 «The Quick Brown
5431 Fox Jumps Over
5432 The Lazy Dogˇ»
5433 "});
5434
5435 // Test multiple line, single selection case
5436 cx.set_state(indoc! {"
5437 «The quick brown
5438 fox jumps over
5439 the lazy dogˇ»
5440 "});
5441 cx.update_editor(|e, window, cx| {
5442 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5443 });
5444 cx.assert_editor_state(indoc! {"
5445 «TheQuickBrown
5446 FoxJumpsOver
5447 TheLazyDogˇ»
5448 "});
5449
5450 // From here on out, test more complex cases of manipulate_text()
5451
5452 // Test no selection case - should affect words cursors are in
5453 // Cursor at beginning, middle, and end of word
5454 cx.set_state(indoc! {"
5455 ˇhello big beauˇtiful worldˇ
5456 "});
5457 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5458 cx.assert_editor_state(indoc! {"
5459 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5460 "});
5461
5462 // Test multiple selections on a single line and across multiple lines
5463 cx.set_state(indoc! {"
5464 «Theˇ» quick «brown
5465 foxˇ» jumps «overˇ»
5466 the «lazyˇ» dog
5467 "});
5468 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5469 cx.assert_editor_state(indoc! {"
5470 «THEˇ» quick «BROWN
5471 FOXˇ» jumps «OVERˇ»
5472 the «LAZYˇ» dog
5473 "});
5474
5475 // Test case where text length grows
5476 cx.set_state(indoc! {"
5477 «tschüߡ»
5478 "});
5479 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5480 cx.assert_editor_state(indoc! {"
5481 «TSCHÜSSˇ»
5482 "});
5483
5484 // Test to make sure we don't crash when text shrinks
5485 cx.set_state(indoc! {"
5486 aaa_bbbˇ
5487 "});
5488 cx.update_editor(|e, window, cx| {
5489 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5490 });
5491 cx.assert_editor_state(indoc! {"
5492 «aaaBbbˇ»
5493 "});
5494
5495 // Test to make sure we all aware of the fact that each word can grow and shrink
5496 // Final selections should be aware of this fact
5497 cx.set_state(indoc! {"
5498 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5499 "});
5500 cx.update_editor(|e, window, cx| {
5501 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5502 });
5503 cx.assert_editor_state(indoc! {"
5504 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5505 "});
5506
5507 cx.set_state(indoc! {"
5508 «hElLo, WoRld!ˇ»
5509 "});
5510 cx.update_editor(|e, window, cx| {
5511 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5512 });
5513 cx.assert_editor_state(indoc! {"
5514 «HeLlO, wOrLD!ˇ»
5515 "});
5516
5517 // Test selections with `line_mode() = true`.
5518 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5519 cx.set_state(indoc! {"
5520 «The quick brown
5521 fox jumps over
5522 tˇ»he lazy dog
5523 "});
5524 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5525 cx.assert_editor_state(indoc! {"
5526 «THE QUICK BROWN
5527 FOX JUMPS OVER
5528 THE LAZY DOGˇ»
5529 "});
5530}
5531
5532#[gpui::test]
5533fn test_duplicate_line(cx: &mut TestAppContext) {
5534 init_test(cx, |_| {});
5535
5536 let editor = cx.add_window(|window, cx| {
5537 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5538 build_editor(buffer, window, cx)
5539 });
5540 _ = editor.update(cx, |editor, window, cx| {
5541 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5542 s.select_display_ranges([
5543 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5544 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5545 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5546 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5547 ])
5548 });
5549 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5550 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5551 assert_eq!(
5552 editor.selections.display_ranges(cx),
5553 vec![
5554 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5555 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5556 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5557 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5558 ]
5559 );
5560 });
5561
5562 let editor = cx.add_window(|window, cx| {
5563 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5564 build_editor(buffer, window, cx)
5565 });
5566 _ = editor.update(cx, |editor, window, cx| {
5567 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5568 s.select_display_ranges([
5569 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5570 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5571 ])
5572 });
5573 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5574 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5575 assert_eq!(
5576 editor.selections.display_ranges(cx),
5577 vec![
5578 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5579 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5580 ]
5581 );
5582 });
5583
5584 // With `move_upwards` the selections stay in place, except for
5585 // the lines inserted above them
5586 let editor = cx.add_window(|window, cx| {
5587 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5588 build_editor(buffer, window, cx)
5589 });
5590 _ = editor.update(cx, |editor, window, cx| {
5591 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5592 s.select_display_ranges([
5593 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5594 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5595 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5596 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5597 ])
5598 });
5599 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5600 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5601 assert_eq!(
5602 editor.selections.display_ranges(cx),
5603 vec![
5604 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5605 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5606 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5607 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5608 ]
5609 );
5610 });
5611
5612 let editor = cx.add_window(|window, cx| {
5613 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5614 build_editor(buffer, window, cx)
5615 });
5616 _ = editor.update(cx, |editor, window, cx| {
5617 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5618 s.select_display_ranges([
5619 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5620 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5621 ])
5622 });
5623 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5624 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5625 assert_eq!(
5626 editor.selections.display_ranges(cx),
5627 vec![
5628 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5629 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5630 ]
5631 );
5632 });
5633
5634 let editor = cx.add_window(|window, cx| {
5635 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5636 build_editor(buffer, window, cx)
5637 });
5638 _ = editor.update(cx, |editor, window, cx| {
5639 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5640 s.select_display_ranges([
5641 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5642 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5643 ])
5644 });
5645 editor.duplicate_selection(&DuplicateSelection, window, cx);
5646 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5647 assert_eq!(
5648 editor.selections.display_ranges(cx),
5649 vec![
5650 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5651 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5652 ]
5653 );
5654 });
5655}
5656
5657#[gpui::test]
5658fn test_move_line_up_down(cx: &mut TestAppContext) {
5659 init_test(cx, |_| {});
5660
5661 let editor = cx.add_window(|window, cx| {
5662 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5663 build_editor(buffer, window, cx)
5664 });
5665 _ = editor.update(cx, |editor, window, cx| {
5666 editor.fold_creases(
5667 vec![
5668 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5669 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5670 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5671 ],
5672 true,
5673 window,
5674 cx,
5675 );
5676 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5677 s.select_display_ranges([
5678 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5679 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5680 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5681 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5682 ])
5683 });
5684 assert_eq!(
5685 editor.display_text(cx),
5686 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5687 );
5688
5689 editor.move_line_up(&MoveLineUp, window, cx);
5690 assert_eq!(
5691 editor.display_text(cx),
5692 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5693 );
5694 assert_eq!(
5695 editor.selections.display_ranges(cx),
5696 vec![
5697 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5698 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5699 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5700 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5701 ]
5702 );
5703 });
5704
5705 _ = editor.update(cx, |editor, window, cx| {
5706 editor.move_line_down(&MoveLineDown, window, cx);
5707 assert_eq!(
5708 editor.display_text(cx),
5709 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5710 );
5711 assert_eq!(
5712 editor.selections.display_ranges(cx),
5713 vec![
5714 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5715 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5716 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5717 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 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\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5727 );
5728 assert_eq!(
5729 editor.selections.display_ranges(cx),
5730 vec![
5731 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 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_up(&MoveLineUp, window, cx);
5741 assert_eq!(
5742 editor.display_text(cx),
5743 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5744 );
5745 assert_eq!(
5746 editor.selections.display_ranges(cx),
5747 vec![
5748 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5749 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5750 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5751 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5752 ]
5753 );
5754 });
5755}
5756
5757#[gpui::test]
5758fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5759 init_test(cx, |_| {});
5760 let editor = cx.add_window(|window, cx| {
5761 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5762 build_editor(buffer, window, cx)
5763 });
5764 _ = editor.update(cx, |editor, window, cx| {
5765 editor.fold_creases(
5766 vec![Crease::simple(
5767 Point::new(6, 4)..Point::new(7, 4),
5768 FoldPlaceholder::test(),
5769 )],
5770 true,
5771 window,
5772 cx,
5773 );
5774 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5775 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5776 });
5777 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5778 editor.move_line_up(&MoveLineUp, window, cx);
5779 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5780 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5781 });
5782}
5783
5784#[gpui::test]
5785fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5786 init_test(cx, |_| {});
5787
5788 let editor = cx.add_window(|window, cx| {
5789 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5790 build_editor(buffer, window, cx)
5791 });
5792 _ = editor.update(cx, |editor, window, cx| {
5793 let snapshot = editor.buffer.read(cx).snapshot(cx);
5794 editor.insert_blocks(
5795 [BlockProperties {
5796 style: BlockStyle::Fixed,
5797 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5798 height: Some(1),
5799 render: Arc::new(|_| div().into_any()),
5800 priority: 0,
5801 }],
5802 Some(Autoscroll::fit()),
5803 cx,
5804 );
5805 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5806 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5807 });
5808 editor.move_line_down(&MoveLineDown, window, cx);
5809 });
5810}
5811
5812#[gpui::test]
5813async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5814 init_test(cx, |_| {});
5815
5816 let mut cx = EditorTestContext::new(cx).await;
5817 cx.set_state(
5818 &"
5819 ˇzero
5820 one
5821 two
5822 three
5823 four
5824 five
5825 "
5826 .unindent(),
5827 );
5828
5829 // Create a four-line block that replaces three lines of text.
5830 cx.update_editor(|editor, window, cx| {
5831 let snapshot = editor.snapshot(window, cx);
5832 let snapshot = &snapshot.buffer_snapshot();
5833 let placement = BlockPlacement::Replace(
5834 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5835 );
5836 editor.insert_blocks(
5837 [BlockProperties {
5838 placement,
5839 height: Some(4),
5840 style: BlockStyle::Sticky,
5841 render: Arc::new(|_| gpui::div().into_any_element()),
5842 priority: 0,
5843 }],
5844 None,
5845 cx,
5846 );
5847 });
5848
5849 // Move down so that the cursor touches the block.
5850 cx.update_editor(|editor, window, cx| {
5851 editor.move_down(&Default::default(), window, cx);
5852 });
5853 cx.assert_editor_state(
5854 &"
5855 zero
5856 «one
5857 two
5858 threeˇ»
5859 four
5860 five
5861 "
5862 .unindent(),
5863 );
5864
5865 // Move down past the block.
5866 cx.update_editor(|editor, window, cx| {
5867 editor.move_down(&Default::default(), window, cx);
5868 });
5869 cx.assert_editor_state(
5870 &"
5871 zero
5872 one
5873 two
5874 three
5875 ˇfour
5876 five
5877 "
5878 .unindent(),
5879 );
5880}
5881
5882#[gpui::test]
5883fn test_transpose(cx: &mut TestAppContext) {
5884 init_test(cx, |_| {});
5885
5886 _ = cx.add_window(|window, cx| {
5887 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5888 editor.set_style(EditorStyle::default(), window, cx);
5889 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5890 s.select_ranges([1..1])
5891 });
5892 editor.transpose(&Default::default(), window, cx);
5893 assert_eq!(editor.text(cx), "bac");
5894 assert_eq!(editor.selections.ranges(cx), [2..2]);
5895
5896 editor.transpose(&Default::default(), window, cx);
5897 assert_eq!(editor.text(cx), "bca");
5898 assert_eq!(editor.selections.ranges(cx), [3..3]);
5899
5900 editor.transpose(&Default::default(), window, cx);
5901 assert_eq!(editor.text(cx), "bac");
5902 assert_eq!(editor.selections.ranges(cx), [3..3]);
5903
5904 editor
5905 });
5906
5907 _ = cx.add_window(|window, cx| {
5908 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5909 editor.set_style(EditorStyle::default(), window, cx);
5910 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5911 s.select_ranges([3..3])
5912 });
5913 editor.transpose(&Default::default(), window, cx);
5914 assert_eq!(editor.text(cx), "acb\nde");
5915 assert_eq!(editor.selections.ranges(cx), [3..3]);
5916
5917 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5918 s.select_ranges([4..4])
5919 });
5920 editor.transpose(&Default::default(), window, cx);
5921 assert_eq!(editor.text(cx), "acbd\ne");
5922 assert_eq!(editor.selections.ranges(cx), [5..5]);
5923
5924 editor.transpose(&Default::default(), window, cx);
5925 assert_eq!(editor.text(cx), "acbde\n");
5926 assert_eq!(editor.selections.ranges(cx), [6..6]);
5927
5928 editor.transpose(&Default::default(), window, cx);
5929 assert_eq!(editor.text(cx), "acbd\ne");
5930 assert_eq!(editor.selections.ranges(cx), [6..6]);
5931
5932 editor
5933 });
5934
5935 _ = cx.add_window(|window, cx| {
5936 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5937 editor.set_style(EditorStyle::default(), window, cx);
5938 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5939 s.select_ranges([1..1, 2..2, 4..4])
5940 });
5941 editor.transpose(&Default::default(), window, cx);
5942 assert_eq!(editor.text(cx), "bacd\ne");
5943 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5944
5945 editor.transpose(&Default::default(), window, cx);
5946 assert_eq!(editor.text(cx), "bcade\n");
5947 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5948
5949 editor.transpose(&Default::default(), window, cx);
5950 assert_eq!(editor.text(cx), "bcda\ne");
5951 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5952
5953 editor.transpose(&Default::default(), window, cx);
5954 assert_eq!(editor.text(cx), "bcade\n");
5955 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5956
5957 editor.transpose(&Default::default(), window, cx);
5958 assert_eq!(editor.text(cx), "bcaed\n");
5959 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5960
5961 editor
5962 });
5963
5964 _ = cx.add_window(|window, cx| {
5965 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5966 editor.set_style(EditorStyle::default(), window, cx);
5967 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5968 s.select_ranges([4..4])
5969 });
5970 editor.transpose(&Default::default(), window, cx);
5971 assert_eq!(editor.text(cx), "🏀🍐✋");
5972 assert_eq!(editor.selections.ranges(cx), [8..8]);
5973
5974 editor.transpose(&Default::default(), window, cx);
5975 assert_eq!(editor.text(cx), "🏀✋🍐");
5976 assert_eq!(editor.selections.ranges(cx), [11..11]);
5977
5978 editor.transpose(&Default::default(), window, cx);
5979 assert_eq!(editor.text(cx), "🏀🍐✋");
5980 assert_eq!(editor.selections.ranges(cx), [11..11]);
5981
5982 editor
5983 });
5984}
5985
5986#[gpui::test]
5987async fn test_rewrap(cx: &mut TestAppContext) {
5988 init_test(cx, |settings| {
5989 settings.languages.0.extend([
5990 (
5991 "Markdown".into(),
5992 LanguageSettingsContent {
5993 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5994 preferred_line_length: Some(40),
5995 ..Default::default()
5996 },
5997 ),
5998 (
5999 "Plain Text".into(),
6000 LanguageSettingsContent {
6001 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6002 preferred_line_length: Some(40),
6003 ..Default::default()
6004 },
6005 ),
6006 (
6007 "C++".into(),
6008 LanguageSettingsContent {
6009 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6010 preferred_line_length: Some(40),
6011 ..Default::default()
6012 },
6013 ),
6014 (
6015 "Python".into(),
6016 LanguageSettingsContent {
6017 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6018 preferred_line_length: Some(40),
6019 ..Default::default()
6020 },
6021 ),
6022 (
6023 "Rust".into(),
6024 LanguageSettingsContent {
6025 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6026 preferred_line_length: Some(40),
6027 ..Default::default()
6028 },
6029 ),
6030 ])
6031 });
6032
6033 let mut cx = EditorTestContext::new(cx).await;
6034
6035 let cpp_language = Arc::new(Language::new(
6036 LanguageConfig {
6037 name: "C++".into(),
6038 line_comments: vec!["// ".into()],
6039 ..LanguageConfig::default()
6040 },
6041 None,
6042 ));
6043 let python_language = Arc::new(Language::new(
6044 LanguageConfig {
6045 name: "Python".into(),
6046 line_comments: vec!["# ".into()],
6047 ..LanguageConfig::default()
6048 },
6049 None,
6050 ));
6051 let markdown_language = Arc::new(Language::new(
6052 LanguageConfig {
6053 name: "Markdown".into(),
6054 rewrap_prefixes: vec![
6055 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6056 regex::Regex::new("[-*+]\\s+").unwrap(),
6057 ],
6058 ..LanguageConfig::default()
6059 },
6060 None,
6061 ));
6062 let rust_language = Arc::new(
6063 Language::new(
6064 LanguageConfig {
6065 name: "Rust".into(),
6066 line_comments: vec!["// ".into(), "/// ".into()],
6067 ..LanguageConfig::default()
6068 },
6069 Some(tree_sitter_rust::LANGUAGE.into()),
6070 )
6071 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6072 .unwrap(),
6073 );
6074
6075 let plaintext_language = Arc::new(Language::new(
6076 LanguageConfig {
6077 name: "Plain Text".into(),
6078 ..LanguageConfig::default()
6079 },
6080 None,
6081 ));
6082
6083 // Test basic rewrapping of a long line with a cursor
6084 assert_rewrap(
6085 indoc! {"
6086 // ˇThis is a long comment that needs to be wrapped.
6087 "},
6088 indoc! {"
6089 // ˇThis is a long comment that needs to
6090 // be wrapped.
6091 "},
6092 cpp_language.clone(),
6093 &mut cx,
6094 );
6095
6096 // Test rewrapping a full selection
6097 assert_rewrap(
6098 indoc! {"
6099 «// This selected long comment needs to be wrapped.ˇ»"
6100 },
6101 indoc! {"
6102 «// This selected long comment needs to
6103 // be wrapped.ˇ»"
6104 },
6105 cpp_language.clone(),
6106 &mut cx,
6107 );
6108
6109 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6110 assert_rewrap(
6111 indoc! {"
6112 // ˇThis is the first line.
6113 // Thisˇ is the second line.
6114 // This is the thirdˇ line, all part of one paragraph.
6115 "},
6116 indoc! {"
6117 // ˇThis is the first line. Thisˇ is the
6118 // second line. This is the thirdˇ line,
6119 // all part of one paragraph.
6120 "},
6121 cpp_language.clone(),
6122 &mut cx,
6123 );
6124
6125 // Test multiple cursors in different paragraphs trigger separate rewraps
6126 assert_rewrap(
6127 indoc! {"
6128 // ˇThis is the first paragraph, first line.
6129 // ˇThis is the first paragraph, second line.
6130
6131 // ˇThis is the second paragraph, first line.
6132 // ˇThis is the second paragraph, second line.
6133 "},
6134 indoc! {"
6135 // ˇThis is the first paragraph, first
6136 // line. ˇThis is the first paragraph,
6137 // second line.
6138
6139 // ˇThis is the second paragraph, first
6140 // line. ˇThis is the second paragraph,
6141 // second line.
6142 "},
6143 cpp_language.clone(),
6144 &mut cx,
6145 );
6146
6147 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6148 assert_rewrap(
6149 indoc! {"
6150 «// A regular long long comment to be wrapped.
6151 /// A documentation long comment to be wrapped.ˇ»
6152 "},
6153 indoc! {"
6154 «// A regular long long comment to be
6155 // wrapped.
6156 /// A documentation long comment to be
6157 /// wrapped.ˇ»
6158 "},
6159 rust_language.clone(),
6160 &mut cx,
6161 );
6162
6163 // Test that change in indentation level trigger seperate rewraps
6164 assert_rewrap(
6165 indoc! {"
6166 fn foo() {
6167 «// This is a long comment at the base indent.
6168 // This is a long comment at the next indent.ˇ»
6169 }
6170 "},
6171 indoc! {"
6172 fn foo() {
6173 «// This is a long comment at the
6174 // base indent.
6175 // This is a long comment at the
6176 // next indent.ˇ»
6177 }
6178 "},
6179 rust_language.clone(),
6180 &mut cx,
6181 );
6182
6183 // Test that different comment prefix characters (e.g., '#') are handled correctly
6184 assert_rewrap(
6185 indoc! {"
6186 # ˇThis is a long comment using a pound sign.
6187 "},
6188 indoc! {"
6189 # ˇThis is a long comment using a pound
6190 # sign.
6191 "},
6192 python_language,
6193 &mut cx,
6194 );
6195
6196 // Test rewrapping only affects comments, not code even when selected
6197 assert_rewrap(
6198 indoc! {"
6199 «/// This doc comment is long and should be wrapped.
6200 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6201 "},
6202 indoc! {"
6203 «/// This doc comment is long and should
6204 /// be wrapped.
6205 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6206 "},
6207 rust_language.clone(),
6208 &mut cx,
6209 );
6210
6211 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6212 assert_rewrap(
6213 indoc! {"
6214 # Header
6215
6216 A long long long line of markdown text to wrap.ˇ
6217 "},
6218 indoc! {"
6219 # Header
6220
6221 A long long long line of markdown text
6222 to wrap.ˇ
6223 "},
6224 markdown_language.clone(),
6225 &mut cx,
6226 );
6227
6228 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6229 assert_rewrap(
6230 indoc! {"
6231 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6232 2. This is a numbered list item that is very long and needs to be wrapped properly.
6233 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6234 "},
6235 indoc! {"
6236 «1. This is a numbered list item that is
6237 very long and needs to be wrapped
6238 properly.
6239 2. This is a numbered list item that is
6240 very long and needs to be wrapped
6241 properly.
6242 - This is an unordered list item that is
6243 also very long and should not merge
6244 with the numbered item.ˇ»
6245 "},
6246 markdown_language.clone(),
6247 &mut cx,
6248 );
6249
6250 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6251 assert_rewrap(
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 with
6261 the numbered item.ˇ»
6262 "},
6263 indoc! {"
6264 «1. This is a numbered list item that is
6265 very long and needs to be wrapped
6266 properly.
6267 2. This is a numbered list item that is
6268 very long and needs to be wrapped
6269 properly.
6270 - This is an unordered list item that is
6271 also very long and should not merge
6272 with the numbered item.ˇ»
6273 "},
6274 markdown_language.clone(),
6275 &mut cx,
6276 );
6277
6278 // Test that rewrapping maintain indents even when they already exists.
6279 assert_rewrap(
6280 indoc! {"
6281 «1. This is a numbered list
6282 item that is very long and needs to be wrapped properly.
6283 2. This is a numbered list
6284 item that is very long and needs to be wrapped properly.
6285 - This is an unordered list item that is also very long and
6286 should not merge with the numbered item.ˇ»
6287 "},
6288 indoc! {"
6289 «1. This is a numbered list item that is
6290 very long and needs to be wrapped
6291 properly.
6292 2. This is a numbered list item that is
6293 very long and needs to be wrapped
6294 properly.
6295 - This is an unordered list item that is
6296 also very long and should not merge
6297 with the numbered item.ˇ»
6298 "},
6299 markdown_language,
6300 &mut cx,
6301 );
6302
6303 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6304 assert_rewrap(
6305 indoc! {"
6306 ˇThis is a very long line of plain text that will be wrapped.
6307 "},
6308 indoc! {"
6309 ˇThis is a very long line of plain text
6310 that will be wrapped.
6311 "},
6312 plaintext_language.clone(),
6313 &mut cx,
6314 );
6315
6316 // Test that non-commented code acts as a paragraph boundary within a selection
6317 assert_rewrap(
6318 indoc! {"
6319 «// This is the first long comment block to be wrapped.
6320 fn my_func(a: u32);
6321 // This is the second long comment block to be wrapped.ˇ»
6322 "},
6323 indoc! {"
6324 «// This is the first long comment block
6325 // to be wrapped.
6326 fn my_func(a: u32);
6327 // This is the second long comment block
6328 // to be wrapped.ˇ»
6329 "},
6330 rust_language,
6331 &mut cx,
6332 );
6333
6334 // Test rewrapping multiple selections, including ones with blank lines or tabs
6335 assert_rewrap(
6336 indoc! {"
6337 «ˇThis is a very long line that will be wrapped.
6338
6339 This is another paragraph in the same selection.»
6340
6341 «\tThis is a very long indented line that will be wrapped.ˇ»
6342 "},
6343 indoc! {"
6344 «ˇThis is a very long line that will be
6345 wrapped.
6346
6347 This is another paragraph in the same
6348 selection.»
6349
6350 «\tThis is a very long indented line
6351 \tthat will be wrapped.ˇ»
6352 "},
6353 plaintext_language,
6354 &mut cx,
6355 );
6356
6357 // Test that an empty comment line acts as a paragraph boundary
6358 assert_rewrap(
6359 indoc! {"
6360 // ˇThis is a long comment that will be wrapped.
6361 //
6362 // And this is another long comment that will also be wrapped.ˇ
6363 "},
6364 indoc! {"
6365 // ˇThis is a long comment that will be
6366 // wrapped.
6367 //
6368 // And this is another long comment that
6369 // will also be wrapped.ˇ
6370 "},
6371 cpp_language,
6372 &mut cx,
6373 );
6374
6375 #[track_caller]
6376 fn assert_rewrap(
6377 unwrapped_text: &str,
6378 wrapped_text: &str,
6379 language: Arc<Language>,
6380 cx: &mut EditorTestContext,
6381 ) {
6382 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6383 cx.set_state(unwrapped_text);
6384 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6385 cx.assert_editor_state(wrapped_text);
6386 }
6387}
6388
6389#[gpui::test]
6390async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6391 init_test(cx, |settings| {
6392 settings.languages.0.extend([(
6393 "Rust".into(),
6394 LanguageSettingsContent {
6395 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6396 preferred_line_length: Some(40),
6397 ..Default::default()
6398 },
6399 )])
6400 });
6401
6402 let mut cx = EditorTestContext::new(cx).await;
6403
6404 let rust_lang = Arc::new(
6405 Language::new(
6406 LanguageConfig {
6407 name: "Rust".into(),
6408 line_comments: vec!["// ".into()],
6409 block_comment: Some(BlockCommentConfig {
6410 start: "/*".into(),
6411 end: "*/".into(),
6412 prefix: "* ".into(),
6413 tab_size: 1,
6414 }),
6415 documentation_comment: Some(BlockCommentConfig {
6416 start: "/**".into(),
6417 end: "*/".into(),
6418 prefix: "* ".into(),
6419 tab_size: 1,
6420 }),
6421
6422 ..LanguageConfig::default()
6423 },
6424 Some(tree_sitter_rust::LANGUAGE.into()),
6425 )
6426 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6427 .unwrap(),
6428 );
6429
6430 // regular block comment
6431 assert_rewrap(
6432 indoc! {"
6433 /*
6434 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6435 */
6436 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6437 "},
6438 indoc! {"
6439 /*
6440 *ˇ Lorem ipsum dolor sit amet,
6441 * consectetur adipiscing elit.
6442 */
6443 /*
6444 *ˇ Lorem ipsum dolor sit amet,
6445 * consectetur adipiscing elit.
6446 */
6447 "},
6448 rust_lang.clone(),
6449 &mut cx,
6450 );
6451
6452 // indent is respected
6453 assert_rewrap(
6454 indoc! {"
6455 {}
6456 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6457 "},
6458 indoc! {"
6459 {}
6460 /*
6461 *ˇ Lorem ipsum dolor sit amet,
6462 * consectetur adipiscing elit.
6463 */
6464 "},
6465 rust_lang.clone(),
6466 &mut cx,
6467 );
6468
6469 // short block comments with inline delimiters
6470 assert_rewrap(
6471 indoc! {"
6472 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6473 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6474 */
6475 /*
6476 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6477 "},
6478 indoc! {"
6479 /*
6480 *ˇ Lorem ipsum dolor sit amet,
6481 * consectetur adipiscing elit.
6482 */
6483 /*
6484 *ˇ Lorem ipsum dolor sit amet,
6485 * consectetur adipiscing elit.
6486 */
6487 /*
6488 *ˇ Lorem ipsum dolor sit amet,
6489 * consectetur adipiscing elit.
6490 */
6491 "},
6492 rust_lang.clone(),
6493 &mut cx,
6494 );
6495
6496 // multiline block comment with inline start/end delimiters
6497 assert_rewrap(
6498 indoc! {"
6499 /*ˇ Lorem ipsum dolor sit amet,
6500 * consectetur adipiscing elit. */
6501 "},
6502 indoc! {"
6503 /*
6504 *ˇ Lorem ipsum dolor sit amet,
6505 * consectetur adipiscing elit.
6506 */
6507 "},
6508 rust_lang.clone(),
6509 &mut cx,
6510 );
6511
6512 // block comment rewrap still respects paragraph bounds
6513 assert_rewrap(
6514 indoc! {"
6515 /*
6516 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6517 *
6518 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6519 */
6520 "},
6521 indoc! {"
6522 /*
6523 *ˇ Lorem ipsum dolor sit amet,
6524 * consectetur adipiscing elit.
6525 *
6526 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6527 */
6528 "},
6529 rust_lang.clone(),
6530 &mut cx,
6531 );
6532
6533 // documentation comments
6534 assert_rewrap(
6535 indoc! {"
6536 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6537 /**
6538 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6539 */
6540 "},
6541 indoc! {"
6542 /**
6543 *ˇ Lorem ipsum dolor sit amet,
6544 * consectetur adipiscing elit.
6545 */
6546 /**
6547 *ˇ Lorem ipsum dolor sit amet,
6548 * consectetur adipiscing elit.
6549 */
6550 "},
6551 rust_lang.clone(),
6552 &mut cx,
6553 );
6554
6555 // different, adjacent comments
6556 assert_rewrap(
6557 indoc! {"
6558 /**
6559 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6560 */
6561 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6562 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6563 "},
6564 indoc! {"
6565 /**
6566 *ˇ Lorem ipsum dolor sit amet,
6567 * consectetur adipiscing elit.
6568 */
6569 /*
6570 *ˇ Lorem ipsum dolor sit amet,
6571 * consectetur adipiscing elit.
6572 */
6573 //ˇ Lorem ipsum dolor sit amet,
6574 // consectetur adipiscing elit.
6575 "},
6576 rust_lang.clone(),
6577 &mut cx,
6578 );
6579
6580 // selection w/ single short block comment
6581 assert_rewrap(
6582 indoc! {"
6583 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6584 "},
6585 indoc! {"
6586 «/*
6587 * Lorem ipsum dolor sit amet,
6588 * consectetur adipiscing elit.
6589 */ˇ»
6590 "},
6591 rust_lang.clone(),
6592 &mut cx,
6593 );
6594
6595 // rewrapping a single comment w/ abutting comments
6596 assert_rewrap(
6597 indoc! {"
6598 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6599 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6600 "},
6601 indoc! {"
6602 /*
6603 * ˇLorem ipsum dolor sit amet,
6604 * consectetur adipiscing elit.
6605 */
6606 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6607 "},
6608 rust_lang.clone(),
6609 &mut cx,
6610 );
6611
6612 // selection w/ non-abutting short block comments
6613 assert_rewrap(
6614 indoc! {"
6615 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6616
6617 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6618 "},
6619 indoc! {"
6620 «/*
6621 * Lorem ipsum dolor sit amet,
6622 * consectetur adipiscing elit.
6623 */
6624
6625 /*
6626 * Lorem ipsum dolor sit amet,
6627 * consectetur adipiscing elit.
6628 */ˇ»
6629 "},
6630 rust_lang.clone(),
6631 &mut cx,
6632 );
6633
6634 // selection of multiline block comments
6635 assert_rewrap(
6636 indoc! {"
6637 «/* Lorem ipsum dolor sit amet,
6638 * consectetur adipiscing elit. */ˇ»
6639 "},
6640 indoc! {"
6641 «/*
6642 * Lorem ipsum dolor sit amet,
6643 * consectetur adipiscing elit.
6644 */ˇ»
6645 "},
6646 rust_lang.clone(),
6647 &mut cx,
6648 );
6649
6650 // partial selection of multiline block comments
6651 assert_rewrap(
6652 indoc! {"
6653 «/* Lorem ipsum dolor sit amet,ˇ»
6654 * consectetur adipiscing elit. */
6655 /* Lorem ipsum dolor sit amet,
6656 «* consectetur adipiscing elit. */ˇ»
6657 "},
6658 indoc! {"
6659 «/*
6660 * Lorem ipsum dolor sit amet,ˇ»
6661 * consectetur adipiscing elit. */
6662 /* Lorem ipsum dolor sit amet,
6663 «* consectetur adipiscing elit.
6664 */ˇ»
6665 "},
6666 rust_lang.clone(),
6667 &mut cx,
6668 );
6669
6670 // selection w/ abutting short block comments
6671 // TODO: should not be combined; should rewrap as 2 comments
6672 assert_rewrap(
6673 indoc! {"
6674 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6675 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6676 "},
6677 // desired behavior:
6678 // indoc! {"
6679 // «/*
6680 // * Lorem ipsum dolor sit amet,
6681 // * consectetur adipiscing elit.
6682 // */
6683 // /*
6684 // * Lorem ipsum dolor sit amet,
6685 // * consectetur adipiscing elit.
6686 // */ˇ»
6687 // "},
6688 // actual behaviour:
6689 indoc! {"
6690 «/*
6691 * Lorem ipsum dolor sit amet,
6692 * consectetur adipiscing elit. Lorem
6693 * ipsum dolor sit amet, consectetur
6694 * adipiscing elit.
6695 */ˇ»
6696 "},
6697 rust_lang.clone(),
6698 &mut cx,
6699 );
6700
6701 // TODO: same as above, but with delimiters on separate line
6702 // assert_rewrap(
6703 // indoc! {"
6704 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6705 // */
6706 // /*
6707 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6708 // "},
6709 // // desired:
6710 // // indoc! {"
6711 // // «/*
6712 // // * Lorem ipsum dolor sit amet,
6713 // // * consectetur adipiscing elit.
6714 // // */
6715 // // /*
6716 // // * Lorem ipsum dolor sit amet,
6717 // // * consectetur adipiscing elit.
6718 // // */ˇ»
6719 // // "},
6720 // // actual: (but with trailing w/s on the empty lines)
6721 // indoc! {"
6722 // «/*
6723 // * Lorem ipsum dolor sit amet,
6724 // * consectetur adipiscing elit.
6725 // *
6726 // */
6727 // /*
6728 // *
6729 // * Lorem ipsum dolor sit amet,
6730 // * consectetur adipiscing elit.
6731 // */ˇ»
6732 // "},
6733 // rust_lang.clone(),
6734 // &mut cx,
6735 // );
6736
6737 // TODO these are unhandled edge cases; not correct, just documenting known issues
6738 assert_rewrap(
6739 indoc! {"
6740 /*
6741 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6742 */
6743 /*
6744 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6745 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6746 "},
6747 // desired:
6748 // indoc! {"
6749 // /*
6750 // *ˇ Lorem ipsum dolor sit amet,
6751 // * consectetur adipiscing elit.
6752 // */
6753 // /*
6754 // *ˇ Lorem ipsum dolor sit amet,
6755 // * consectetur adipiscing elit.
6756 // */
6757 // /*
6758 // *ˇ Lorem ipsum dolor sit amet
6759 // */ /* consectetur adipiscing elit. */
6760 // "},
6761 // actual:
6762 indoc! {"
6763 /*
6764 //ˇ Lorem ipsum dolor sit amet,
6765 // consectetur adipiscing elit.
6766 */
6767 /*
6768 * //ˇ Lorem ipsum dolor sit amet,
6769 * consectetur adipiscing elit.
6770 */
6771 /*
6772 *ˇ Lorem ipsum dolor sit amet */ /*
6773 * consectetur adipiscing elit.
6774 */
6775 "},
6776 rust_lang,
6777 &mut cx,
6778 );
6779
6780 #[track_caller]
6781 fn assert_rewrap(
6782 unwrapped_text: &str,
6783 wrapped_text: &str,
6784 language: Arc<Language>,
6785 cx: &mut EditorTestContext,
6786 ) {
6787 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6788 cx.set_state(unwrapped_text);
6789 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6790 cx.assert_editor_state(wrapped_text);
6791 }
6792}
6793
6794#[gpui::test]
6795async fn test_hard_wrap(cx: &mut TestAppContext) {
6796 init_test(cx, |_| {});
6797 let mut cx = EditorTestContext::new(cx).await;
6798
6799 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6800 cx.update_editor(|editor, _, cx| {
6801 editor.set_hard_wrap(Some(14), cx);
6802 });
6803
6804 cx.set_state(indoc!(
6805 "
6806 one two three ˇ
6807 "
6808 ));
6809 cx.simulate_input("four");
6810 cx.run_until_parked();
6811
6812 cx.assert_editor_state(indoc!(
6813 "
6814 one two three
6815 fourˇ
6816 "
6817 ));
6818
6819 cx.update_editor(|editor, window, cx| {
6820 editor.newline(&Default::default(), window, cx);
6821 });
6822 cx.run_until_parked();
6823 cx.assert_editor_state(indoc!(
6824 "
6825 one two three
6826 four
6827 ˇ
6828 "
6829 ));
6830
6831 cx.simulate_input("five");
6832 cx.run_until_parked();
6833 cx.assert_editor_state(indoc!(
6834 "
6835 one two three
6836 four
6837 fiveˇ
6838 "
6839 ));
6840
6841 cx.update_editor(|editor, window, cx| {
6842 editor.newline(&Default::default(), window, cx);
6843 });
6844 cx.run_until_parked();
6845 cx.simulate_input("# ");
6846 cx.run_until_parked();
6847 cx.assert_editor_state(indoc!(
6848 "
6849 one two three
6850 four
6851 five
6852 # ˇ
6853 "
6854 ));
6855
6856 cx.update_editor(|editor, window, cx| {
6857 editor.newline(&Default::default(), window, cx);
6858 });
6859 cx.run_until_parked();
6860 cx.assert_editor_state(indoc!(
6861 "
6862 one two three
6863 four
6864 five
6865 #\x20
6866 #ˇ
6867 "
6868 ));
6869
6870 cx.simulate_input(" 6");
6871 cx.run_until_parked();
6872 cx.assert_editor_state(indoc!(
6873 "
6874 one two three
6875 four
6876 five
6877 #
6878 # 6ˇ
6879 "
6880 ));
6881}
6882
6883#[gpui::test]
6884async fn test_cut_line_ends(cx: &mut TestAppContext) {
6885 init_test(cx, |_| {});
6886
6887 let mut cx = EditorTestContext::new(cx).await;
6888
6889 cx.set_state(indoc! {"The quick brownˇ"});
6890 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6891 cx.assert_editor_state(indoc! {"The quick brownˇ"});
6892
6893 cx.set_state(indoc! {"The emacs foxˇ"});
6894 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6895 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
6896
6897 cx.set_state(indoc! {"
6898 The quick« brownˇ»
6899 fox jumps overˇ
6900 the lazy dog"});
6901 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6902 cx.assert_editor_state(indoc! {"
6903 The quickˇ
6904 ˇthe lazy dog"});
6905
6906 cx.set_state(indoc! {"
6907 The quick« brownˇ»
6908 fox jumps overˇ
6909 the lazy dog"});
6910 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6911 cx.assert_editor_state(indoc! {"
6912 The quickˇ
6913 fox jumps overˇthe lazy dog"});
6914
6915 cx.set_state(indoc! {"
6916 The quick« brownˇ»
6917 fox jumps overˇ
6918 the lazy dog"});
6919 cx.update_editor(|e, window, cx| {
6920 e.cut_to_end_of_line(
6921 &CutToEndOfLine {
6922 stop_at_newlines: true,
6923 },
6924 window,
6925 cx,
6926 )
6927 });
6928 cx.assert_editor_state(indoc! {"
6929 The quickˇ
6930 fox jumps overˇ
6931 the lazy dog"});
6932
6933 cx.set_state(indoc! {"
6934 The quick« brownˇ»
6935 fox jumps overˇ
6936 the lazy dog"});
6937 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6938 cx.assert_editor_state(indoc! {"
6939 The quickˇ
6940 fox jumps overˇthe lazy dog"});
6941}
6942
6943#[gpui::test]
6944async fn test_clipboard(cx: &mut TestAppContext) {
6945 init_test(cx, |_| {});
6946
6947 let mut cx = EditorTestContext::new(cx).await;
6948
6949 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
6950 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6951 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
6952
6953 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
6954 cx.set_state("two ˇfour ˇsix ˇ");
6955 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6956 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
6957
6958 // Paste again but with only two cursors. Since the number of cursors doesn't
6959 // match the number of slices in the clipboard, the entire clipboard text
6960 // is pasted at each cursor.
6961 cx.set_state("ˇtwo one✅ four three six five ˇ");
6962 cx.update_editor(|e, window, cx| {
6963 e.handle_input("( ", window, cx);
6964 e.paste(&Paste, window, cx);
6965 e.handle_input(") ", window, cx);
6966 });
6967 cx.assert_editor_state(
6968 &([
6969 "( one✅ ",
6970 "three ",
6971 "five ) ˇtwo one✅ four three six five ( one✅ ",
6972 "three ",
6973 "five ) ˇ",
6974 ]
6975 .join("\n")),
6976 );
6977
6978 // Cut with three selections, one of which is full-line.
6979 cx.set_state(indoc! {"
6980 1«2ˇ»3
6981 4ˇ567
6982 «8ˇ»9"});
6983 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6984 cx.assert_editor_state(indoc! {"
6985 1ˇ3
6986 ˇ9"});
6987
6988 // Paste with three selections, noticing how the copied selection that was full-line
6989 // gets inserted before the second cursor.
6990 cx.set_state(indoc! {"
6991 1ˇ3
6992 9ˇ
6993 «oˇ»ne"});
6994 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6995 cx.assert_editor_state(indoc! {"
6996 12ˇ3
6997 4567
6998 9ˇ
6999 8ˇne"});
7000
7001 // Copy with a single cursor only, which writes the whole line into the clipboard.
7002 cx.set_state(indoc! {"
7003 The quick brown
7004 fox juˇmps over
7005 the lazy dog"});
7006 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7007 assert_eq!(
7008 cx.read_from_clipboard()
7009 .and_then(|item| item.text().as_deref().map(str::to_string)),
7010 Some("fox jumps over\n".to_string())
7011 );
7012
7013 // Paste with three selections, noticing how the copied full-line selection is inserted
7014 // before the empty selections but replaces the selection that is non-empty.
7015 cx.set_state(indoc! {"
7016 Tˇhe quick brown
7017 «foˇ»x jumps over
7018 tˇhe lazy dog"});
7019 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7020 cx.assert_editor_state(indoc! {"
7021 fox jumps over
7022 Tˇhe quick brown
7023 fox jumps over
7024 ˇx jumps over
7025 fox jumps over
7026 tˇhe lazy dog"});
7027}
7028
7029#[gpui::test]
7030async fn test_copy_trim(cx: &mut TestAppContext) {
7031 init_test(cx, |_| {});
7032
7033 let mut cx = EditorTestContext::new(cx).await;
7034 cx.set_state(
7035 r#" «for selection in selections.iter() {
7036 let mut start = selection.start;
7037 let mut end = selection.end;
7038 let is_entire_line = selection.is_empty();
7039 if is_entire_line {
7040 start = Point::new(start.row, 0);ˇ»
7041 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7042 }
7043 "#,
7044 );
7045 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7046 assert_eq!(
7047 cx.read_from_clipboard()
7048 .and_then(|item| item.text().as_deref().map(str::to_string)),
7049 Some(
7050 "for selection in selections.iter() {
7051 let mut start = selection.start;
7052 let mut end = selection.end;
7053 let is_entire_line = selection.is_empty();
7054 if is_entire_line {
7055 start = Point::new(start.row, 0);"
7056 .to_string()
7057 ),
7058 "Regular copying preserves all indentation selected",
7059 );
7060 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7061 assert_eq!(
7062 cx.read_from_clipboard()
7063 .and_then(|item| item.text().as_deref().map(str::to_string)),
7064 Some(
7065 "for selection in selections.iter() {
7066let mut start = selection.start;
7067let mut end = selection.end;
7068let is_entire_line = selection.is_empty();
7069if is_entire_line {
7070 start = Point::new(start.row, 0);"
7071 .to_string()
7072 ),
7073 "Copying with stripping should strip all leading whitespaces"
7074 );
7075
7076 cx.set_state(
7077 r#" « for selection in selections.iter() {
7078 let mut start = selection.start;
7079 let mut end = selection.end;
7080 let is_entire_line = selection.is_empty();
7081 if is_entire_line {
7082 start = Point::new(start.row, 0);ˇ»
7083 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7084 }
7085 "#,
7086 );
7087 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7088 assert_eq!(
7089 cx.read_from_clipboard()
7090 .and_then(|item| item.text().as_deref().map(str::to_string)),
7091 Some(
7092 " for selection in selections.iter() {
7093 let mut start = selection.start;
7094 let mut end = selection.end;
7095 let is_entire_line = selection.is_empty();
7096 if is_entire_line {
7097 start = Point::new(start.row, 0);"
7098 .to_string()
7099 ),
7100 "Regular copying preserves all indentation selected",
7101 );
7102 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7103 assert_eq!(
7104 cx.read_from_clipboard()
7105 .and_then(|item| item.text().as_deref().map(str::to_string)),
7106 Some(
7107 "for selection in selections.iter() {
7108let mut start = selection.start;
7109let mut end = selection.end;
7110let is_entire_line = selection.is_empty();
7111if is_entire_line {
7112 start = Point::new(start.row, 0);"
7113 .to_string()
7114 ),
7115 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7116 );
7117
7118 cx.set_state(
7119 r#" «ˇ for selection in selections.iter() {
7120 let mut start = selection.start;
7121 let mut end = selection.end;
7122 let is_entire_line = selection.is_empty();
7123 if is_entire_line {
7124 start = Point::new(start.row, 0);»
7125 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7126 }
7127 "#,
7128 );
7129 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7130 assert_eq!(
7131 cx.read_from_clipboard()
7132 .and_then(|item| item.text().as_deref().map(str::to_string)),
7133 Some(
7134 " for selection in selections.iter() {
7135 let mut start = selection.start;
7136 let mut end = selection.end;
7137 let is_entire_line = selection.is_empty();
7138 if is_entire_line {
7139 start = Point::new(start.row, 0);"
7140 .to_string()
7141 ),
7142 "Regular copying for reverse selection works the same",
7143 );
7144 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7145 assert_eq!(
7146 cx.read_from_clipboard()
7147 .and_then(|item| item.text().as_deref().map(str::to_string)),
7148 Some(
7149 "for selection in selections.iter() {
7150let mut start = selection.start;
7151let mut end = selection.end;
7152let is_entire_line = selection.is_empty();
7153if is_entire_line {
7154 start = Point::new(start.row, 0);"
7155 .to_string()
7156 ),
7157 "Copying with stripping for reverse selection works the same"
7158 );
7159
7160 cx.set_state(
7161 r#" for selection «in selections.iter() {
7162 let mut start = selection.start;
7163 let mut end = selection.end;
7164 let is_entire_line = selection.is_empty();
7165 if is_entire_line {
7166 start = Point::new(start.row, 0);ˇ»
7167 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7168 }
7169 "#,
7170 );
7171 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7172 assert_eq!(
7173 cx.read_from_clipboard()
7174 .and_then(|item| item.text().as_deref().map(str::to_string)),
7175 Some(
7176 "in selections.iter() {
7177 let mut start = selection.start;
7178 let mut end = selection.end;
7179 let is_entire_line = selection.is_empty();
7180 if is_entire_line {
7181 start = Point::new(start.row, 0);"
7182 .to_string()
7183 ),
7184 "When selecting past the indent, the copying works as usual",
7185 );
7186 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7187 assert_eq!(
7188 cx.read_from_clipboard()
7189 .and_then(|item| item.text().as_deref().map(str::to_string)),
7190 Some(
7191 "in selections.iter() {
7192 let mut start = selection.start;
7193 let mut end = selection.end;
7194 let is_entire_line = selection.is_empty();
7195 if is_entire_line {
7196 start = Point::new(start.row, 0);"
7197 .to_string()
7198 ),
7199 "When selecting past the indent, nothing is trimmed"
7200 );
7201
7202 cx.set_state(
7203 r#" «for selection in selections.iter() {
7204 let mut start = selection.start;
7205
7206 let mut end = selection.end;
7207 let is_entire_line = selection.is_empty();
7208 if is_entire_line {
7209 start = Point::new(start.row, 0);
7210ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7211 }
7212 "#,
7213 );
7214 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7215 assert_eq!(
7216 cx.read_from_clipboard()
7217 .and_then(|item| item.text().as_deref().map(str::to_string)),
7218 Some(
7219 "for selection in selections.iter() {
7220let mut start = selection.start;
7221
7222let mut end = selection.end;
7223let is_entire_line = selection.is_empty();
7224if is_entire_line {
7225 start = Point::new(start.row, 0);
7226"
7227 .to_string()
7228 ),
7229 "Copying with stripping should ignore empty lines"
7230 );
7231}
7232
7233#[gpui::test]
7234async fn test_paste_multiline(cx: &mut TestAppContext) {
7235 init_test(cx, |_| {});
7236
7237 let mut cx = EditorTestContext::new(cx).await;
7238 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7239
7240 // Cut an indented block, without the leading whitespace.
7241 cx.set_state(indoc! {"
7242 const a: B = (
7243 c(),
7244 «d(
7245 e,
7246 f
7247 )ˇ»
7248 );
7249 "});
7250 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7251 cx.assert_editor_state(indoc! {"
7252 const a: B = (
7253 c(),
7254 ˇ
7255 );
7256 "});
7257
7258 // Paste it at the same position.
7259 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7260 cx.assert_editor_state(indoc! {"
7261 const a: B = (
7262 c(),
7263 d(
7264 e,
7265 f
7266 )ˇ
7267 );
7268 "});
7269
7270 // Paste it at a line with a lower indent level.
7271 cx.set_state(indoc! {"
7272 ˇ
7273 const a: B = (
7274 c(),
7275 );
7276 "});
7277 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7278 cx.assert_editor_state(indoc! {"
7279 d(
7280 e,
7281 f
7282 )ˇ
7283 const a: B = (
7284 c(),
7285 );
7286 "});
7287
7288 // Cut an indented block, with the leading whitespace.
7289 cx.set_state(indoc! {"
7290 const a: B = (
7291 c(),
7292 « d(
7293 e,
7294 f
7295 )
7296 ˇ»);
7297 "});
7298 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7299 cx.assert_editor_state(indoc! {"
7300 const a: B = (
7301 c(),
7302 ˇ);
7303 "});
7304
7305 // Paste it at the same position.
7306 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7307 cx.assert_editor_state(indoc! {"
7308 const a: B = (
7309 c(),
7310 d(
7311 e,
7312 f
7313 )
7314 ˇ);
7315 "});
7316
7317 // Paste it at a line with a higher indent level.
7318 cx.set_state(indoc! {"
7319 const a: B = (
7320 c(),
7321 d(
7322 e,
7323 fˇ
7324 )
7325 );
7326 "});
7327 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7328 cx.assert_editor_state(indoc! {"
7329 const a: B = (
7330 c(),
7331 d(
7332 e,
7333 f d(
7334 e,
7335 f
7336 )
7337 ˇ
7338 )
7339 );
7340 "});
7341
7342 // Copy an indented block, starting mid-line
7343 cx.set_state(indoc! {"
7344 const a: B = (
7345 c(),
7346 somethin«g(
7347 e,
7348 f
7349 )ˇ»
7350 );
7351 "});
7352 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7353
7354 // Paste it on a line with a lower indent level
7355 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7356 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7357 cx.assert_editor_state(indoc! {"
7358 const a: B = (
7359 c(),
7360 something(
7361 e,
7362 f
7363 )
7364 );
7365 g(
7366 e,
7367 f
7368 )ˇ"});
7369}
7370
7371#[gpui::test]
7372async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7373 init_test(cx, |_| {});
7374
7375 cx.write_to_clipboard(ClipboardItem::new_string(
7376 " d(\n e\n );\n".into(),
7377 ));
7378
7379 let mut cx = EditorTestContext::new(cx).await;
7380 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7381
7382 cx.set_state(indoc! {"
7383 fn a() {
7384 b();
7385 if c() {
7386 ˇ
7387 }
7388 }
7389 "});
7390
7391 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7392 cx.assert_editor_state(indoc! {"
7393 fn a() {
7394 b();
7395 if c() {
7396 d(
7397 e
7398 );
7399 ˇ
7400 }
7401 }
7402 "});
7403
7404 cx.set_state(indoc! {"
7405 fn a() {
7406 b();
7407 ˇ
7408 }
7409 "});
7410
7411 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7412 cx.assert_editor_state(indoc! {"
7413 fn a() {
7414 b();
7415 d(
7416 e
7417 );
7418 ˇ
7419 }
7420 "});
7421}
7422
7423#[gpui::test]
7424fn test_select_all(cx: &mut TestAppContext) {
7425 init_test(cx, |_| {});
7426
7427 let editor = cx.add_window(|window, cx| {
7428 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7429 build_editor(buffer, window, cx)
7430 });
7431 _ = editor.update(cx, |editor, window, cx| {
7432 editor.select_all(&SelectAll, window, cx);
7433 assert_eq!(
7434 editor.selections.display_ranges(cx),
7435 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7436 );
7437 });
7438}
7439
7440#[gpui::test]
7441fn test_select_line(cx: &mut TestAppContext) {
7442 init_test(cx, |_| {});
7443
7444 let editor = cx.add_window(|window, cx| {
7445 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7446 build_editor(buffer, window, cx)
7447 });
7448 _ = editor.update(cx, |editor, window, cx| {
7449 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7450 s.select_display_ranges([
7451 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7452 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7453 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7454 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7455 ])
7456 });
7457 editor.select_line(&SelectLine, window, cx);
7458 assert_eq!(
7459 editor.selections.display_ranges(cx),
7460 vec![
7461 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7462 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7463 ]
7464 );
7465 });
7466
7467 _ = editor.update(cx, |editor, window, cx| {
7468 editor.select_line(&SelectLine, window, cx);
7469 assert_eq!(
7470 editor.selections.display_ranges(cx),
7471 vec![
7472 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7473 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7474 ]
7475 );
7476 });
7477
7478 _ = editor.update(cx, |editor, window, cx| {
7479 editor.select_line(&SelectLine, window, cx);
7480 assert_eq!(
7481 editor.selections.display_ranges(cx),
7482 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7483 );
7484 });
7485}
7486
7487#[gpui::test]
7488async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7489 init_test(cx, |_| {});
7490 let mut cx = EditorTestContext::new(cx).await;
7491
7492 #[track_caller]
7493 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7494 cx.set_state(initial_state);
7495 cx.update_editor(|e, window, cx| {
7496 e.split_selection_into_lines(&Default::default(), window, cx)
7497 });
7498 cx.assert_editor_state(expected_state);
7499 }
7500
7501 // Selection starts and ends at the middle of lines, left-to-right
7502 test(
7503 &mut cx,
7504 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7505 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7506 );
7507 // Same thing, right-to-left
7508 test(
7509 &mut cx,
7510 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7511 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7512 );
7513
7514 // Whole buffer, left-to-right, last line *doesn't* end with newline
7515 test(
7516 &mut cx,
7517 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7518 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7519 );
7520 // Same thing, right-to-left
7521 test(
7522 &mut cx,
7523 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7524 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7525 );
7526
7527 // Whole buffer, left-to-right, last line ends with newline
7528 test(
7529 &mut cx,
7530 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7531 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7532 );
7533 // Same thing, right-to-left
7534 test(
7535 &mut cx,
7536 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7537 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7538 );
7539
7540 // Starts at the end of a line, ends at the start of another
7541 test(
7542 &mut cx,
7543 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7544 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7545 );
7546}
7547
7548#[gpui::test]
7549async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7550 init_test(cx, |_| {});
7551
7552 let editor = cx.add_window(|window, cx| {
7553 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7554 build_editor(buffer, window, cx)
7555 });
7556
7557 // setup
7558 _ = editor.update(cx, |editor, window, cx| {
7559 editor.fold_creases(
7560 vec![
7561 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7562 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7563 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7564 ],
7565 true,
7566 window,
7567 cx,
7568 );
7569 assert_eq!(
7570 editor.display_text(cx),
7571 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7572 );
7573 });
7574
7575 _ = editor.update(cx, |editor, window, cx| {
7576 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7577 s.select_display_ranges([
7578 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7579 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7580 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7581 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7582 ])
7583 });
7584 editor.split_selection_into_lines(&Default::default(), window, cx);
7585 assert_eq!(
7586 editor.display_text(cx),
7587 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7588 );
7589 });
7590 EditorTestContext::for_editor(editor, cx)
7591 .await
7592 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7593
7594 _ = editor.update(cx, |editor, window, cx| {
7595 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7596 s.select_display_ranges([
7597 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7598 ])
7599 });
7600 editor.split_selection_into_lines(&Default::default(), window, cx);
7601 assert_eq!(
7602 editor.display_text(cx),
7603 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7604 );
7605 assert_eq!(
7606 editor.selections.display_ranges(cx),
7607 [
7608 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7609 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7610 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7611 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7612 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7613 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7614 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7615 ]
7616 );
7617 });
7618 EditorTestContext::for_editor(editor, cx)
7619 .await
7620 .assert_editor_state(
7621 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7622 );
7623}
7624
7625#[gpui::test]
7626async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7627 init_test(cx, |_| {});
7628
7629 let mut cx = EditorTestContext::new(cx).await;
7630
7631 cx.set_state(indoc!(
7632 r#"abc
7633 defˇghi
7634
7635 jk
7636 nlmo
7637 "#
7638 ));
7639
7640 cx.update_editor(|editor, window, cx| {
7641 editor.add_selection_above(&Default::default(), window, cx);
7642 });
7643
7644 cx.assert_editor_state(indoc!(
7645 r#"abcˇ
7646 defˇghi
7647
7648 jk
7649 nlmo
7650 "#
7651 ));
7652
7653 cx.update_editor(|editor, window, cx| {
7654 editor.add_selection_above(&Default::default(), window, cx);
7655 });
7656
7657 cx.assert_editor_state(indoc!(
7658 r#"abcˇ
7659 defˇghi
7660
7661 jk
7662 nlmo
7663 "#
7664 ));
7665
7666 cx.update_editor(|editor, window, cx| {
7667 editor.add_selection_below(&Default::default(), window, cx);
7668 });
7669
7670 cx.assert_editor_state(indoc!(
7671 r#"abc
7672 defˇghi
7673
7674 jk
7675 nlmo
7676 "#
7677 ));
7678
7679 cx.update_editor(|editor, window, cx| {
7680 editor.undo_selection(&Default::default(), window, cx);
7681 });
7682
7683 cx.assert_editor_state(indoc!(
7684 r#"abcˇ
7685 defˇghi
7686
7687 jk
7688 nlmo
7689 "#
7690 ));
7691
7692 cx.update_editor(|editor, window, cx| {
7693 editor.redo_selection(&Default::default(), window, cx);
7694 });
7695
7696 cx.assert_editor_state(indoc!(
7697 r#"abc
7698 defˇghi
7699
7700 jk
7701 nlmo
7702 "#
7703 ));
7704
7705 cx.update_editor(|editor, window, cx| {
7706 editor.add_selection_below(&Default::default(), window, cx);
7707 });
7708
7709 cx.assert_editor_state(indoc!(
7710 r#"abc
7711 defˇghi
7712 ˇ
7713 jk
7714 nlmo
7715 "#
7716 ));
7717
7718 cx.update_editor(|editor, window, cx| {
7719 editor.add_selection_below(&Default::default(), window, cx);
7720 });
7721
7722 cx.assert_editor_state(indoc!(
7723 r#"abc
7724 defˇghi
7725 ˇ
7726 jkˇ
7727 nlmo
7728 "#
7729 ));
7730
7731 cx.update_editor(|editor, window, cx| {
7732 editor.add_selection_below(&Default::default(), window, cx);
7733 });
7734
7735 cx.assert_editor_state(indoc!(
7736 r#"abc
7737 defˇghi
7738 ˇ
7739 jkˇ
7740 nlmˇo
7741 "#
7742 ));
7743
7744 cx.update_editor(|editor, window, cx| {
7745 editor.add_selection_below(&Default::default(), window, cx);
7746 });
7747
7748 cx.assert_editor_state(indoc!(
7749 r#"abc
7750 defˇghi
7751 ˇ
7752 jkˇ
7753 nlmˇo
7754 ˇ"#
7755 ));
7756
7757 // change selections
7758 cx.set_state(indoc!(
7759 r#"abc
7760 def«ˇg»hi
7761
7762 jk
7763 nlmo
7764 "#
7765 ));
7766
7767 cx.update_editor(|editor, window, cx| {
7768 editor.add_selection_below(&Default::default(), window, cx);
7769 });
7770
7771 cx.assert_editor_state(indoc!(
7772 r#"abc
7773 def«ˇg»hi
7774
7775 jk
7776 nlm«ˇo»
7777 "#
7778 ));
7779
7780 cx.update_editor(|editor, window, cx| {
7781 editor.add_selection_below(&Default::default(), window, cx);
7782 });
7783
7784 cx.assert_editor_state(indoc!(
7785 r#"abc
7786 def«ˇg»hi
7787
7788 jk
7789 nlm«ˇo»
7790 "#
7791 ));
7792
7793 cx.update_editor(|editor, window, cx| {
7794 editor.add_selection_above(&Default::default(), window, cx);
7795 });
7796
7797 cx.assert_editor_state(indoc!(
7798 r#"abc
7799 def«ˇg»hi
7800
7801 jk
7802 nlmo
7803 "#
7804 ));
7805
7806 cx.update_editor(|editor, window, cx| {
7807 editor.add_selection_above(&Default::default(), window, cx);
7808 });
7809
7810 cx.assert_editor_state(indoc!(
7811 r#"abc
7812 def«ˇg»hi
7813
7814 jk
7815 nlmo
7816 "#
7817 ));
7818
7819 // Change selections again
7820 cx.set_state(indoc!(
7821 r#"a«bc
7822 defgˇ»hi
7823
7824 jk
7825 nlmo
7826 "#
7827 ));
7828
7829 cx.update_editor(|editor, window, cx| {
7830 editor.add_selection_below(&Default::default(), window, cx);
7831 });
7832
7833 cx.assert_editor_state(indoc!(
7834 r#"a«bcˇ»
7835 d«efgˇ»hi
7836
7837 j«kˇ»
7838 nlmo
7839 "#
7840 ));
7841
7842 cx.update_editor(|editor, window, cx| {
7843 editor.add_selection_below(&Default::default(), window, cx);
7844 });
7845 cx.assert_editor_state(indoc!(
7846 r#"a«bcˇ»
7847 d«efgˇ»hi
7848
7849 j«kˇ»
7850 n«lmoˇ»
7851 "#
7852 ));
7853 cx.update_editor(|editor, window, cx| {
7854 editor.add_selection_above(&Default::default(), window, cx);
7855 });
7856
7857 cx.assert_editor_state(indoc!(
7858 r#"a«bcˇ»
7859 d«efgˇ»hi
7860
7861 j«kˇ»
7862 nlmo
7863 "#
7864 ));
7865
7866 // Change selections again
7867 cx.set_state(indoc!(
7868 r#"abc
7869 d«ˇefghi
7870
7871 jk
7872 nlm»o
7873 "#
7874 ));
7875
7876 cx.update_editor(|editor, window, cx| {
7877 editor.add_selection_above(&Default::default(), window, cx);
7878 });
7879
7880 cx.assert_editor_state(indoc!(
7881 r#"a«ˇbc»
7882 d«ˇef»ghi
7883
7884 j«ˇk»
7885 n«ˇlm»o
7886 "#
7887 ));
7888
7889 cx.update_editor(|editor, window, cx| {
7890 editor.add_selection_below(&Default::default(), window, cx);
7891 });
7892
7893 cx.assert_editor_state(indoc!(
7894 r#"abc
7895 d«ˇef»ghi
7896
7897 j«ˇk»
7898 n«ˇlm»o
7899 "#
7900 ));
7901}
7902
7903#[gpui::test]
7904async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
7905 init_test(cx, |_| {});
7906 let mut cx = EditorTestContext::new(cx).await;
7907
7908 cx.set_state(indoc!(
7909 r#"line onˇe
7910 liˇne two
7911 line three
7912 line four"#
7913 ));
7914
7915 cx.update_editor(|editor, window, cx| {
7916 editor.add_selection_below(&Default::default(), window, cx);
7917 });
7918
7919 // test multiple cursors expand in the same direction
7920 cx.assert_editor_state(indoc!(
7921 r#"line onˇe
7922 liˇne twˇo
7923 liˇne three
7924 line four"#
7925 ));
7926
7927 cx.update_editor(|editor, window, cx| {
7928 editor.add_selection_below(&Default::default(), window, cx);
7929 });
7930
7931 cx.update_editor(|editor, window, cx| {
7932 editor.add_selection_below(&Default::default(), window, cx);
7933 });
7934
7935 // test multiple cursors expand below overflow
7936 cx.assert_editor_state(indoc!(
7937 r#"line onˇe
7938 liˇne twˇo
7939 liˇne thˇree
7940 liˇne foˇur"#
7941 ));
7942
7943 cx.update_editor(|editor, window, cx| {
7944 editor.add_selection_above(&Default::default(), window, cx);
7945 });
7946
7947 // test multiple cursors retrieves back correctly
7948 cx.assert_editor_state(indoc!(
7949 r#"line onˇe
7950 liˇne twˇo
7951 liˇne thˇree
7952 line four"#
7953 ));
7954
7955 cx.update_editor(|editor, window, cx| {
7956 editor.add_selection_above(&Default::default(), window, cx);
7957 });
7958
7959 cx.update_editor(|editor, window, cx| {
7960 editor.add_selection_above(&Default::default(), window, cx);
7961 });
7962
7963 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
7964 cx.assert_editor_state(indoc!(
7965 r#"liˇne onˇe
7966 liˇne two
7967 line three
7968 line four"#
7969 ));
7970
7971 cx.update_editor(|editor, window, cx| {
7972 editor.undo_selection(&Default::default(), window, cx);
7973 });
7974
7975 // test undo
7976 cx.assert_editor_state(indoc!(
7977 r#"line onˇe
7978 liˇne twˇo
7979 line three
7980 line four"#
7981 ));
7982
7983 cx.update_editor(|editor, window, cx| {
7984 editor.redo_selection(&Default::default(), window, cx);
7985 });
7986
7987 // test redo
7988 cx.assert_editor_state(indoc!(
7989 r#"liˇne onˇe
7990 liˇne two
7991 line three
7992 line four"#
7993 ));
7994
7995 cx.set_state(indoc!(
7996 r#"abcd
7997 ef«ghˇ»
7998 ijkl
7999 «mˇ»nop"#
8000 ));
8001
8002 cx.update_editor(|editor, window, cx| {
8003 editor.add_selection_above(&Default::default(), window, cx);
8004 });
8005
8006 // test multiple selections expand in the same direction
8007 cx.assert_editor_state(indoc!(
8008 r#"ab«cdˇ»
8009 ef«ghˇ»
8010 «iˇ»jkl
8011 «mˇ»nop"#
8012 ));
8013
8014 cx.update_editor(|editor, window, cx| {
8015 editor.add_selection_above(&Default::default(), window, cx);
8016 });
8017
8018 // test multiple selection upward overflow
8019 cx.assert_editor_state(indoc!(
8020 r#"ab«cdˇ»
8021 «eˇ»f«ghˇ»
8022 «iˇ»jkl
8023 «mˇ»nop"#
8024 ));
8025
8026 cx.update_editor(|editor, window, cx| {
8027 editor.add_selection_below(&Default::default(), window, cx);
8028 });
8029
8030 // test multiple selection retrieves back correctly
8031 cx.assert_editor_state(indoc!(
8032 r#"abcd
8033 ef«ghˇ»
8034 «iˇ»jkl
8035 «mˇ»nop"#
8036 ));
8037
8038 cx.update_editor(|editor, window, cx| {
8039 editor.add_selection_below(&Default::default(), window, cx);
8040 });
8041
8042 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8043 cx.assert_editor_state(indoc!(
8044 r#"abcd
8045 ef«ghˇ»
8046 ij«klˇ»
8047 «mˇ»nop"#
8048 ));
8049
8050 cx.update_editor(|editor, window, cx| {
8051 editor.undo_selection(&Default::default(), window, cx);
8052 });
8053
8054 // test undo
8055 cx.assert_editor_state(indoc!(
8056 r#"abcd
8057 ef«ghˇ»
8058 «iˇ»jkl
8059 «mˇ»nop"#
8060 ));
8061
8062 cx.update_editor(|editor, window, cx| {
8063 editor.redo_selection(&Default::default(), window, cx);
8064 });
8065
8066 // test redo
8067 cx.assert_editor_state(indoc!(
8068 r#"abcd
8069 ef«ghˇ»
8070 ij«klˇ»
8071 «mˇ»nop"#
8072 ));
8073}
8074
8075#[gpui::test]
8076async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8077 init_test(cx, |_| {});
8078 let mut cx = EditorTestContext::new(cx).await;
8079
8080 cx.set_state(indoc!(
8081 r#"line onˇe
8082 liˇne two
8083 line three
8084 line four"#
8085 ));
8086
8087 cx.update_editor(|editor, window, cx| {
8088 editor.add_selection_below(&Default::default(), window, cx);
8089 editor.add_selection_below(&Default::default(), window, cx);
8090 editor.add_selection_below(&Default::default(), window, cx);
8091 });
8092
8093 // initial state with two multi cursor groups
8094 cx.assert_editor_state(indoc!(
8095 r#"line onˇe
8096 liˇne twˇo
8097 liˇne thˇree
8098 liˇne foˇur"#
8099 ));
8100
8101 // add single cursor in middle - simulate opt click
8102 cx.update_editor(|editor, window, cx| {
8103 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8104 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8105 editor.end_selection(window, cx);
8106 });
8107
8108 cx.assert_editor_state(indoc!(
8109 r#"line onˇe
8110 liˇne twˇo
8111 liˇneˇ thˇree
8112 liˇne foˇur"#
8113 ));
8114
8115 cx.update_editor(|editor, window, cx| {
8116 editor.add_selection_above(&Default::default(), window, cx);
8117 });
8118
8119 // test new added selection expands above and existing selection shrinks
8120 cx.assert_editor_state(indoc!(
8121 r#"line onˇe
8122 liˇneˇ twˇo
8123 liˇneˇ thˇree
8124 line four"#
8125 ));
8126
8127 cx.update_editor(|editor, window, cx| {
8128 editor.add_selection_above(&Default::default(), window, cx);
8129 });
8130
8131 // test new added selection expands above and existing selection shrinks
8132 cx.assert_editor_state(indoc!(
8133 r#"lineˇ onˇe
8134 liˇneˇ twˇo
8135 lineˇ three
8136 line four"#
8137 ));
8138
8139 // intial state with two selection groups
8140 cx.set_state(indoc!(
8141 r#"abcd
8142 ef«ghˇ»
8143 ijkl
8144 «mˇ»nop"#
8145 ));
8146
8147 cx.update_editor(|editor, window, cx| {
8148 editor.add_selection_above(&Default::default(), window, cx);
8149 editor.add_selection_above(&Default::default(), window, cx);
8150 });
8151
8152 cx.assert_editor_state(indoc!(
8153 r#"ab«cdˇ»
8154 «eˇ»f«ghˇ»
8155 «iˇ»jkl
8156 «mˇ»nop"#
8157 ));
8158
8159 // add single selection in middle - simulate opt drag
8160 cx.update_editor(|editor, window, cx| {
8161 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8162 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8163 editor.update_selection(
8164 DisplayPoint::new(DisplayRow(2), 4),
8165 0,
8166 gpui::Point::<f32>::default(),
8167 window,
8168 cx,
8169 );
8170 editor.end_selection(window, cx);
8171 });
8172
8173 cx.assert_editor_state(indoc!(
8174 r#"ab«cdˇ»
8175 «eˇ»f«ghˇ»
8176 «iˇ»jk«lˇ»
8177 «mˇ»nop"#
8178 ));
8179
8180 cx.update_editor(|editor, window, cx| {
8181 editor.add_selection_below(&Default::default(), window, cx);
8182 });
8183
8184 // test new added selection expands below, others shrinks from above
8185 cx.assert_editor_state(indoc!(
8186 r#"abcd
8187 ef«ghˇ»
8188 «iˇ»jk«lˇ»
8189 «mˇ»no«pˇ»"#
8190 ));
8191}
8192
8193#[gpui::test]
8194async fn test_select_next(cx: &mut TestAppContext) {
8195 init_test(cx, |_| {});
8196
8197 let mut cx = EditorTestContext::new(cx).await;
8198 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8199
8200 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8201 .unwrap();
8202 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8203
8204 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8205 .unwrap();
8206 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8207
8208 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8209 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8210
8211 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8212 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8213
8214 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8215 .unwrap();
8216 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8217
8218 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8219 .unwrap();
8220 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8221
8222 // Test selection direction should be preserved
8223 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8224
8225 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8226 .unwrap();
8227 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8228}
8229
8230#[gpui::test]
8231async fn test_select_all_matches(cx: &mut TestAppContext) {
8232 init_test(cx, |_| {});
8233
8234 let mut cx = EditorTestContext::new(cx).await;
8235
8236 // Test caret-only selections
8237 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8238 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8239 .unwrap();
8240 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8241
8242 // Test left-to-right selections
8243 cx.set_state("abc\n«abcˇ»\nabc");
8244 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8245 .unwrap();
8246 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8247
8248 // Test right-to-left selections
8249 cx.set_state("abc\n«ˇabc»\nabc");
8250 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8251 .unwrap();
8252 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8253
8254 // Test selecting whitespace with caret selection
8255 cx.set_state("abc\nˇ abc\nabc");
8256 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8257 .unwrap();
8258 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8259
8260 // Test selecting whitespace with left-to-right selection
8261 cx.set_state("abc\n«ˇ »abc\nabc");
8262 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8263 .unwrap();
8264 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8265
8266 // Test no matches with right-to-left selection
8267 cx.set_state("abc\n« ˇ»abc\nabc");
8268 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8269 .unwrap();
8270 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8271
8272 // Test with a single word and clip_at_line_ends=true (#29823)
8273 cx.set_state("aˇbc");
8274 cx.update_editor(|e, window, cx| {
8275 e.set_clip_at_line_ends(true, cx);
8276 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8277 e.set_clip_at_line_ends(false, cx);
8278 });
8279 cx.assert_editor_state("«abcˇ»");
8280}
8281
8282#[gpui::test]
8283async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8284 init_test(cx, |_| {});
8285
8286 let mut cx = EditorTestContext::new(cx).await;
8287
8288 let large_body_1 = "\nd".repeat(200);
8289 let large_body_2 = "\ne".repeat(200);
8290
8291 cx.set_state(&format!(
8292 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8293 ));
8294 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8295 let scroll_position = editor.scroll_position(cx);
8296 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8297 scroll_position
8298 });
8299
8300 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8301 .unwrap();
8302 cx.assert_editor_state(&format!(
8303 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8304 ));
8305 let scroll_position_after_selection =
8306 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8307 assert_eq!(
8308 initial_scroll_position, scroll_position_after_selection,
8309 "Scroll position should not change after selecting all matches"
8310 );
8311}
8312
8313#[gpui::test]
8314async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8315 init_test(cx, |_| {});
8316
8317 let mut cx = EditorLspTestContext::new_rust(
8318 lsp::ServerCapabilities {
8319 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8320 ..Default::default()
8321 },
8322 cx,
8323 )
8324 .await;
8325
8326 cx.set_state(indoc! {"
8327 line 1
8328 line 2
8329 linˇe 3
8330 line 4
8331 line 5
8332 "});
8333
8334 // Make an edit
8335 cx.update_editor(|editor, window, cx| {
8336 editor.handle_input("X", window, cx);
8337 });
8338
8339 // Move cursor to a different position
8340 cx.update_editor(|editor, window, cx| {
8341 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8342 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8343 });
8344 });
8345
8346 cx.assert_editor_state(indoc! {"
8347 line 1
8348 line 2
8349 linXe 3
8350 line 4
8351 liˇne 5
8352 "});
8353
8354 cx.lsp
8355 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8356 Ok(Some(vec![lsp::TextEdit::new(
8357 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8358 "PREFIX ".to_string(),
8359 )]))
8360 });
8361
8362 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8363 .unwrap()
8364 .await
8365 .unwrap();
8366
8367 cx.assert_editor_state(indoc! {"
8368 PREFIX line 1
8369 line 2
8370 linXe 3
8371 line 4
8372 liˇne 5
8373 "});
8374
8375 // Undo formatting
8376 cx.update_editor(|editor, window, cx| {
8377 editor.undo(&Default::default(), window, cx);
8378 });
8379
8380 // Verify cursor moved back to position after edit
8381 cx.assert_editor_state(indoc! {"
8382 line 1
8383 line 2
8384 linXˇe 3
8385 line 4
8386 line 5
8387 "});
8388}
8389
8390#[gpui::test]
8391async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8392 init_test(cx, |_| {});
8393
8394 let mut cx = EditorTestContext::new(cx).await;
8395
8396 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8397 cx.update_editor(|editor, window, cx| {
8398 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8399 });
8400
8401 cx.set_state(indoc! {"
8402 line 1
8403 line 2
8404 linˇe 3
8405 line 4
8406 line 5
8407 line 6
8408 line 7
8409 line 8
8410 line 9
8411 line 10
8412 "});
8413
8414 let snapshot = cx.buffer_snapshot();
8415 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8416
8417 cx.update(|_, cx| {
8418 provider.update(cx, |provider, _| {
8419 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8420 id: None,
8421 edits: vec![(edit_position..edit_position, "X".into())],
8422 edit_preview: None,
8423 }))
8424 })
8425 });
8426
8427 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8428 cx.update_editor(|editor, window, cx| {
8429 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8430 });
8431
8432 cx.assert_editor_state(indoc! {"
8433 line 1
8434 line 2
8435 lineXˇ 3
8436 line 4
8437 line 5
8438 line 6
8439 line 7
8440 line 8
8441 line 9
8442 line 10
8443 "});
8444
8445 cx.update_editor(|editor, window, cx| {
8446 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8447 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8448 });
8449 });
8450
8451 cx.assert_editor_state(indoc! {"
8452 line 1
8453 line 2
8454 lineX 3
8455 line 4
8456 line 5
8457 line 6
8458 line 7
8459 line 8
8460 line 9
8461 liˇne 10
8462 "});
8463
8464 cx.update_editor(|editor, window, cx| {
8465 editor.undo(&Default::default(), window, cx);
8466 });
8467
8468 cx.assert_editor_state(indoc! {"
8469 line 1
8470 line 2
8471 lineˇ 3
8472 line 4
8473 line 5
8474 line 6
8475 line 7
8476 line 8
8477 line 9
8478 line 10
8479 "});
8480}
8481
8482#[gpui::test]
8483async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8484 init_test(cx, |_| {});
8485
8486 let mut cx = EditorTestContext::new(cx).await;
8487 cx.set_state(
8488 r#"let foo = 2;
8489lˇet foo = 2;
8490let fooˇ = 2;
8491let foo = 2;
8492let foo = ˇ2;"#,
8493 );
8494
8495 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8496 .unwrap();
8497 cx.assert_editor_state(
8498 r#"let foo = 2;
8499«letˇ» foo = 2;
8500let «fooˇ» = 2;
8501let foo = 2;
8502let foo = «2ˇ»;"#,
8503 );
8504
8505 // noop for multiple selections with different contents
8506 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8507 .unwrap();
8508 cx.assert_editor_state(
8509 r#"let foo = 2;
8510«letˇ» foo = 2;
8511let «fooˇ» = 2;
8512let foo = 2;
8513let foo = «2ˇ»;"#,
8514 );
8515
8516 // Test last selection direction should be preserved
8517 cx.set_state(
8518 r#"let foo = 2;
8519let foo = 2;
8520let «fooˇ» = 2;
8521let «ˇfoo» = 2;
8522let foo = 2;"#,
8523 );
8524
8525 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8526 .unwrap();
8527 cx.assert_editor_state(
8528 r#"let foo = 2;
8529let foo = 2;
8530let «fooˇ» = 2;
8531let «ˇfoo» = 2;
8532let «ˇfoo» = 2;"#,
8533 );
8534}
8535
8536#[gpui::test]
8537async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8538 init_test(cx, |_| {});
8539
8540 let mut cx =
8541 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8542
8543 cx.assert_editor_state(indoc! {"
8544 ˇbbb
8545 ccc
8546
8547 bbb
8548 ccc
8549 "});
8550 cx.dispatch_action(SelectPrevious::default());
8551 cx.assert_editor_state(indoc! {"
8552 «bbbˇ»
8553 ccc
8554
8555 bbb
8556 ccc
8557 "});
8558 cx.dispatch_action(SelectPrevious::default());
8559 cx.assert_editor_state(indoc! {"
8560 «bbbˇ»
8561 ccc
8562
8563 «bbbˇ»
8564 ccc
8565 "});
8566}
8567
8568#[gpui::test]
8569async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8570 init_test(cx, |_| {});
8571
8572 let mut cx = EditorTestContext::new(cx).await;
8573 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8574
8575 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8576 .unwrap();
8577 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8578
8579 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8580 .unwrap();
8581 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8582
8583 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8584 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8585
8586 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8587 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8588
8589 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8590 .unwrap();
8591 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8592
8593 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8594 .unwrap();
8595 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8596}
8597
8598#[gpui::test]
8599async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8600 init_test(cx, |_| {});
8601
8602 let mut cx = EditorTestContext::new(cx).await;
8603 cx.set_state("aˇ");
8604
8605 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8606 .unwrap();
8607 cx.assert_editor_state("«aˇ»");
8608 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8609 .unwrap();
8610 cx.assert_editor_state("«aˇ»");
8611}
8612
8613#[gpui::test]
8614async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8615 init_test(cx, |_| {});
8616
8617 let mut cx = EditorTestContext::new(cx).await;
8618 cx.set_state(
8619 r#"let foo = 2;
8620lˇet foo = 2;
8621let fooˇ = 2;
8622let foo = 2;
8623let foo = ˇ2;"#,
8624 );
8625
8626 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8627 .unwrap();
8628 cx.assert_editor_state(
8629 r#"let foo = 2;
8630«letˇ» foo = 2;
8631let «fooˇ» = 2;
8632let foo = 2;
8633let foo = «2ˇ»;"#,
8634 );
8635
8636 // noop for multiple selections with different contents
8637 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8638 .unwrap();
8639 cx.assert_editor_state(
8640 r#"let foo = 2;
8641«letˇ» foo = 2;
8642let «fooˇ» = 2;
8643let foo = 2;
8644let foo = «2ˇ»;"#,
8645 );
8646}
8647
8648#[gpui::test]
8649async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8650 init_test(cx, |_| {});
8651
8652 let mut cx = EditorTestContext::new(cx).await;
8653 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8654
8655 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8656 .unwrap();
8657 // selection direction is preserved
8658 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8659
8660 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8661 .unwrap();
8662 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8663
8664 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8665 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8666
8667 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8668 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8669
8670 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8671 .unwrap();
8672 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8673
8674 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8675 .unwrap();
8676 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8677}
8678
8679#[gpui::test]
8680async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8681 init_test(cx, |_| {});
8682
8683 let language = Arc::new(Language::new(
8684 LanguageConfig::default(),
8685 Some(tree_sitter_rust::LANGUAGE.into()),
8686 ));
8687
8688 let text = r#"
8689 use mod1::mod2::{mod3, mod4};
8690
8691 fn fn_1(param1: bool, param2: &str) {
8692 let var1 = "text";
8693 }
8694 "#
8695 .unindent();
8696
8697 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8698 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8699 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8700
8701 editor
8702 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8703 .await;
8704
8705 editor.update_in(cx, |editor, window, cx| {
8706 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8707 s.select_display_ranges([
8708 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8709 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8710 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8711 ]);
8712 });
8713 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8714 });
8715 editor.update(cx, |editor, cx| {
8716 assert_text_with_selections(
8717 editor,
8718 indoc! {r#"
8719 use mod1::mod2::{mod3, «mod4ˇ»};
8720
8721 fn fn_1«ˇ(param1: bool, param2: &str)» {
8722 let var1 = "«ˇtext»";
8723 }
8724 "#},
8725 cx,
8726 );
8727 });
8728
8729 editor.update_in(cx, |editor, window, cx| {
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 assert_eq!(
8750 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8751 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8752 );
8753
8754 // Trying to expand the selected syntax node one more time has no effect.
8755 editor.update_in(cx, |editor, window, cx| {
8756 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8757 });
8758 assert_eq!(
8759 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8760 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8761 );
8762
8763 editor.update_in(cx, |editor, window, cx| {
8764 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8765 });
8766 editor.update(cx, |editor, cx| {
8767 assert_text_with_selections(
8768 editor,
8769 indoc! {r#"
8770 use mod1::mod2::«{mod3, mod4}ˇ»;
8771
8772 «ˇfn fn_1(param1: bool, param2: &str) {
8773 let var1 = "text";
8774 }»
8775 "#},
8776 cx,
8777 );
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, moˇd4};
8805
8806 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8807 let var1 = "teˇxt";
8808 }
8809 "#},
8810 cx,
8811 );
8812 });
8813
8814 // Trying to shrink the selected syntax node one more time has no effect.
8815 editor.update_in(cx, |editor, window, cx| {
8816 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8817 });
8818 editor.update_in(cx, |editor, _, cx| {
8819 assert_text_with_selections(
8820 editor,
8821 indoc! {r#"
8822 use mod1::mod2::{mod3, moˇd4};
8823
8824 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8825 let var1 = "teˇxt";
8826 }
8827 "#},
8828 cx,
8829 );
8830 });
8831
8832 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8833 // a fold.
8834 editor.update_in(cx, |editor, window, cx| {
8835 editor.fold_creases(
8836 vec![
8837 Crease::simple(
8838 Point::new(0, 21)..Point::new(0, 24),
8839 FoldPlaceholder::test(),
8840 ),
8841 Crease::simple(
8842 Point::new(3, 20)..Point::new(3, 22),
8843 FoldPlaceholder::test(),
8844 ),
8845 ],
8846 true,
8847 window,
8848 cx,
8849 );
8850 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8851 });
8852 editor.update(cx, |editor, cx| {
8853 assert_text_with_selections(
8854 editor,
8855 indoc! {r#"
8856 use mod1::mod2::«{mod3, mod4}ˇ»;
8857
8858 fn fn_1«ˇ(param1: bool, param2: &str)» {
8859 let var1 = "«ˇtext»";
8860 }
8861 "#},
8862 cx,
8863 );
8864 });
8865}
8866
8867#[gpui::test]
8868async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8869 init_test(cx, |_| {});
8870
8871 let language = Arc::new(Language::new(
8872 LanguageConfig::default(),
8873 Some(tree_sitter_rust::LANGUAGE.into()),
8874 ));
8875
8876 let text = "let a = 2;";
8877
8878 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8879 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8880 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8881
8882 editor
8883 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8884 .await;
8885
8886 // Test case 1: Cursor at end of word
8887 editor.update_in(cx, |editor, window, cx| {
8888 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8889 s.select_display_ranges([
8890 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
8891 ]);
8892 });
8893 });
8894 editor.update(cx, |editor, cx| {
8895 assert_text_with_selections(editor, "let aˇ = 2;", cx);
8896 });
8897 editor.update_in(cx, |editor, window, cx| {
8898 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8899 });
8900 editor.update(cx, |editor, cx| {
8901 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
8902 });
8903 editor.update_in(cx, |editor, window, cx| {
8904 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8905 });
8906 editor.update(cx, |editor, cx| {
8907 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8908 });
8909
8910 // Test case 2: Cursor at end of statement
8911 editor.update_in(cx, |editor, window, cx| {
8912 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8913 s.select_display_ranges([
8914 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
8915 ]);
8916 });
8917 });
8918 editor.update(cx, |editor, cx| {
8919 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
8920 });
8921 editor.update_in(cx, |editor, window, cx| {
8922 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8923 });
8924 editor.update(cx, |editor, cx| {
8925 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8926 });
8927}
8928
8929#[gpui::test]
8930async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
8931 init_test(cx, |_| {});
8932
8933 let language = Arc::new(Language::new(
8934 LanguageConfig {
8935 name: "JavaScript".into(),
8936 ..Default::default()
8937 },
8938 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8939 ));
8940
8941 let text = r#"
8942 let a = {
8943 key: "value",
8944 };
8945 "#
8946 .unindent();
8947
8948 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8949 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8950 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8951
8952 editor
8953 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8954 .await;
8955
8956 // Test case 1: Cursor after '{'
8957 editor.update_in(cx, |editor, window, cx| {
8958 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8959 s.select_display_ranges([
8960 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
8961 ]);
8962 });
8963 });
8964 editor.update(cx, |editor, cx| {
8965 assert_text_with_selections(
8966 editor,
8967 indoc! {r#"
8968 let a = {ˇ
8969 key: "value",
8970 };
8971 "#},
8972 cx,
8973 );
8974 });
8975 editor.update_in(cx, |editor, window, cx| {
8976 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8977 });
8978 editor.update(cx, |editor, cx| {
8979 assert_text_with_selections(
8980 editor,
8981 indoc! {r#"
8982 let a = «ˇ{
8983 key: "value",
8984 }»;
8985 "#},
8986 cx,
8987 );
8988 });
8989
8990 // Test case 2: Cursor after ':'
8991 editor.update_in(cx, |editor, window, cx| {
8992 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8993 s.select_display_ranges([
8994 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
8995 ]);
8996 });
8997 });
8998 editor.update(cx, |editor, cx| {
8999 assert_text_with_selections(
9000 editor,
9001 indoc! {r#"
9002 let a = {
9003 key:ˇ "value",
9004 };
9005 "#},
9006 cx,
9007 );
9008 });
9009 editor.update_in(cx, |editor, window, cx| {
9010 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9011 });
9012 editor.update(cx, |editor, cx| {
9013 assert_text_with_selections(
9014 editor,
9015 indoc! {r#"
9016 let a = {
9017 «ˇkey: "value"»,
9018 };
9019 "#},
9020 cx,
9021 );
9022 });
9023 editor.update_in(cx, |editor, window, cx| {
9024 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9025 });
9026 editor.update(cx, |editor, cx| {
9027 assert_text_with_selections(
9028 editor,
9029 indoc! {r#"
9030 let a = «ˇ{
9031 key: "value",
9032 }»;
9033 "#},
9034 cx,
9035 );
9036 });
9037
9038 // Test case 3: Cursor after ','
9039 editor.update_in(cx, |editor, window, cx| {
9040 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9041 s.select_display_ranges([
9042 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9043 ]);
9044 });
9045 });
9046 editor.update(cx, |editor, cx| {
9047 assert_text_with_selections(
9048 editor,
9049 indoc! {r#"
9050 let a = {
9051 key: "value",ˇ
9052 };
9053 "#},
9054 cx,
9055 );
9056 });
9057 editor.update_in(cx, |editor, window, cx| {
9058 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9059 });
9060 editor.update(cx, |editor, cx| {
9061 assert_text_with_selections(
9062 editor,
9063 indoc! {r#"
9064 let a = «ˇ{
9065 key: "value",
9066 }»;
9067 "#},
9068 cx,
9069 );
9070 });
9071
9072 // Test case 4: Cursor after ';'
9073 editor.update_in(cx, |editor, window, cx| {
9074 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9075 s.select_display_ranges([
9076 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9077 ]);
9078 });
9079 });
9080 editor.update(cx, |editor, cx| {
9081 assert_text_with_selections(
9082 editor,
9083 indoc! {r#"
9084 let a = {
9085 key: "value",
9086 };ˇ
9087 "#},
9088 cx,
9089 );
9090 });
9091 editor.update_in(cx, |editor, window, cx| {
9092 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9093 });
9094 editor.update(cx, |editor, cx| {
9095 assert_text_with_selections(
9096 editor,
9097 indoc! {r#"
9098 «ˇlet a = {
9099 key: "value",
9100 };
9101 »"#},
9102 cx,
9103 );
9104 });
9105}
9106
9107#[gpui::test]
9108async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9109 init_test(cx, |_| {});
9110
9111 let language = Arc::new(Language::new(
9112 LanguageConfig::default(),
9113 Some(tree_sitter_rust::LANGUAGE.into()),
9114 ));
9115
9116 let text = r#"
9117 use mod1::mod2::{mod3, mod4};
9118
9119 fn fn_1(param1: bool, param2: &str) {
9120 let var1 = "hello world";
9121 }
9122 "#
9123 .unindent();
9124
9125 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9126 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9127 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9128
9129 editor
9130 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9131 .await;
9132
9133 // Test 1: Cursor on a letter of a string word
9134 editor.update_in(cx, |editor, window, cx| {
9135 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9136 s.select_display_ranges([
9137 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9138 ]);
9139 });
9140 });
9141 editor.update_in(cx, |editor, window, cx| {
9142 assert_text_with_selections(
9143 editor,
9144 indoc! {r#"
9145 use mod1::mod2::{mod3, mod4};
9146
9147 fn fn_1(param1: bool, param2: &str) {
9148 let var1 = "hˇello world";
9149 }
9150 "#},
9151 cx,
9152 );
9153 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9154 assert_text_with_selections(
9155 editor,
9156 indoc! {r#"
9157 use mod1::mod2::{mod3, mod4};
9158
9159 fn fn_1(param1: bool, param2: &str) {
9160 let var1 = "«ˇhello» world";
9161 }
9162 "#},
9163 cx,
9164 );
9165 });
9166
9167 // Test 2: Partial selection within a word
9168 editor.update_in(cx, |editor, window, cx| {
9169 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9170 s.select_display_ranges([
9171 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9172 ]);
9173 });
9174 });
9175 editor.update_in(cx, |editor, window, cx| {
9176 assert_text_with_selections(
9177 editor,
9178 indoc! {r#"
9179 use mod1::mod2::{mod3, mod4};
9180
9181 fn fn_1(param1: bool, param2: &str) {
9182 let var1 = "h«elˇ»lo world";
9183 }
9184 "#},
9185 cx,
9186 );
9187 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9188 assert_text_with_selections(
9189 editor,
9190 indoc! {r#"
9191 use mod1::mod2::{mod3, mod4};
9192
9193 fn fn_1(param1: bool, param2: &str) {
9194 let var1 = "«ˇhello» world";
9195 }
9196 "#},
9197 cx,
9198 );
9199 });
9200
9201 // Test 3: Complete word already selected
9202 editor.update_in(cx, |editor, window, cx| {
9203 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9204 s.select_display_ranges([
9205 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9206 ]);
9207 });
9208 });
9209 editor.update_in(cx, |editor, window, cx| {
9210 assert_text_with_selections(
9211 editor,
9212 indoc! {r#"
9213 use mod1::mod2::{mod3, mod4};
9214
9215 fn fn_1(param1: bool, param2: &str) {
9216 let var1 = "«helloˇ» world";
9217 }
9218 "#},
9219 cx,
9220 );
9221 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9222 assert_text_with_selections(
9223 editor,
9224 indoc! {r#"
9225 use mod1::mod2::{mod3, mod4};
9226
9227 fn fn_1(param1: bool, param2: &str) {
9228 let var1 = "«hello worldˇ»";
9229 }
9230 "#},
9231 cx,
9232 );
9233 });
9234
9235 // Test 4: Selection spanning across words
9236 editor.update_in(cx, |editor, window, cx| {
9237 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9238 s.select_display_ranges([
9239 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9240 ]);
9241 });
9242 });
9243 editor.update_in(cx, |editor, window, cx| {
9244 assert_text_with_selections(
9245 editor,
9246 indoc! {r#"
9247 use mod1::mod2::{mod3, mod4};
9248
9249 fn fn_1(param1: bool, param2: &str) {
9250 let var1 = "hel«lo woˇ»rld";
9251 }
9252 "#},
9253 cx,
9254 );
9255 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9256 assert_text_with_selections(
9257 editor,
9258 indoc! {r#"
9259 use mod1::mod2::{mod3, mod4};
9260
9261 fn fn_1(param1: bool, param2: &str) {
9262 let var1 = "«ˇhello world»";
9263 }
9264 "#},
9265 cx,
9266 );
9267 });
9268
9269 // Test 5: Expansion beyond string
9270 editor.update_in(cx, |editor, window, cx| {
9271 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
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
9287#[gpui::test]
9288async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9289 init_test(cx, |_| {});
9290
9291 let mut cx = EditorTestContext::new(cx).await;
9292
9293 let language = Arc::new(Language::new(
9294 LanguageConfig::default(),
9295 Some(tree_sitter_rust::LANGUAGE.into()),
9296 ));
9297
9298 cx.update_buffer(|buffer, cx| {
9299 buffer.set_language(Some(language), cx);
9300 });
9301
9302 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9303 cx.update_editor(|editor, window, cx| {
9304 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9305 });
9306
9307 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9308
9309 cx.set_state(indoc! { r#"fn a() {
9310 // what
9311 // a
9312 // ˇlong
9313 // method
9314 // I
9315 // sure
9316 // hope
9317 // it
9318 // works
9319 }"# });
9320
9321 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9322 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9323 cx.update(|_, cx| {
9324 multi_buffer.update(cx, |multi_buffer, cx| {
9325 multi_buffer.set_excerpts_for_path(
9326 PathKey::for_buffer(&buffer, cx),
9327 buffer,
9328 [Point::new(1, 0)..Point::new(1, 0)],
9329 3,
9330 cx,
9331 );
9332 });
9333 });
9334
9335 let editor2 = cx.new_window_entity(|window, cx| {
9336 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9337 });
9338
9339 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9340 cx.update_editor(|editor, window, cx| {
9341 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9342 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9343 })
9344 });
9345
9346 cx.assert_editor_state(indoc! { "
9347 fn a() {
9348 // what
9349 // a
9350 ˇ // long
9351 // method"});
9352
9353 cx.update_editor(|editor, window, cx| {
9354 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9355 });
9356
9357 // Although we could potentially make the action work when the syntax node
9358 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9359 // did. Maybe we could also expand the excerpt to contain the range?
9360 cx.assert_editor_state(indoc! { "
9361 fn a() {
9362 // what
9363 // a
9364 ˇ // long
9365 // method"});
9366}
9367
9368#[gpui::test]
9369async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9370 init_test(cx, |_| {});
9371
9372 let base_text = r#"
9373 impl A {
9374 // this is an uncommitted comment
9375
9376 fn b() {
9377 c();
9378 }
9379
9380 // this is another uncommitted comment
9381
9382 fn d() {
9383 // e
9384 // f
9385 }
9386 }
9387
9388 fn g() {
9389 // h
9390 }
9391 "#
9392 .unindent();
9393
9394 let text = r#"
9395 ˇimpl A {
9396
9397 fn b() {
9398 c();
9399 }
9400
9401 fn d() {
9402 // e
9403 // f
9404 }
9405 }
9406
9407 fn g() {
9408 // h
9409 }
9410 "#
9411 .unindent();
9412
9413 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9414 cx.set_state(&text);
9415 cx.set_head_text(&base_text);
9416 cx.update_editor(|editor, window, cx| {
9417 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9418 });
9419
9420 cx.assert_state_with_diff(
9421 "
9422 ˇimpl A {
9423 - // this is an uncommitted comment
9424
9425 fn b() {
9426 c();
9427 }
9428
9429 - // this is another uncommitted comment
9430 -
9431 fn d() {
9432 // e
9433 // f
9434 }
9435 }
9436
9437 fn g() {
9438 // h
9439 }
9440 "
9441 .unindent(),
9442 );
9443
9444 let expected_display_text = "
9445 impl A {
9446 // this is an uncommitted comment
9447
9448 fn b() {
9449 ⋯
9450 }
9451
9452 // this is another uncommitted comment
9453
9454 fn d() {
9455 ⋯
9456 }
9457 }
9458
9459 fn g() {
9460 ⋯
9461 }
9462 "
9463 .unindent();
9464
9465 cx.update_editor(|editor, window, cx| {
9466 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9467 assert_eq!(editor.display_text(cx), expected_display_text);
9468 });
9469}
9470
9471#[gpui::test]
9472async fn test_autoindent(cx: &mut TestAppContext) {
9473 init_test(cx, |_| {});
9474
9475 let language = Arc::new(
9476 Language::new(
9477 LanguageConfig {
9478 brackets: BracketPairConfig {
9479 pairs: vec![
9480 BracketPair {
9481 start: "{".to_string(),
9482 end: "}".to_string(),
9483 close: false,
9484 surround: false,
9485 newline: true,
9486 },
9487 BracketPair {
9488 start: "(".to_string(),
9489 end: ")".to_string(),
9490 close: false,
9491 surround: false,
9492 newline: true,
9493 },
9494 ],
9495 ..Default::default()
9496 },
9497 ..Default::default()
9498 },
9499 Some(tree_sitter_rust::LANGUAGE.into()),
9500 )
9501 .with_indents_query(
9502 r#"
9503 (_ "(" ")" @end) @indent
9504 (_ "{" "}" @end) @indent
9505 "#,
9506 )
9507 .unwrap(),
9508 );
9509
9510 let text = "fn a() {}";
9511
9512 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9513 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9514 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9515 editor
9516 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9517 .await;
9518
9519 editor.update_in(cx, |editor, window, cx| {
9520 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9521 s.select_ranges([5..5, 8..8, 9..9])
9522 });
9523 editor.newline(&Newline, window, cx);
9524 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9525 assert_eq!(
9526 editor.selections.ranges(cx),
9527 &[
9528 Point::new(1, 4)..Point::new(1, 4),
9529 Point::new(3, 4)..Point::new(3, 4),
9530 Point::new(5, 0)..Point::new(5, 0)
9531 ]
9532 );
9533 });
9534}
9535
9536#[gpui::test]
9537async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9538 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9539
9540 let language = Arc::new(
9541 Language::new(
9542 LanguageConfig {
9543 brackets: BracketPairConfig {
9544 pairs: vec![
9545 BracketPair {
9546 start: "{".to_string(),
9547 end: "}".to_string(),
9548 close: false,
9549 surround: false,
9550 newline: true,
9551 },
9552 BracketPair {
9553 start: "(".to_string(),
9554 end: ")".to_string(),
9555 close: false,
9556 surround: false,
9557 newline: true,
9558 },
9559 ],
9560 ..Default::default()
9561 },
9562 ..Default::default()
9563 },
9564 Some(tree_sitter_rust::LANGUAGE.into()),
9565 )
9566 .with_indents_query(
9567 r#"
9568 (_ "(" ")" @end) @indent
9569 (_ "{" "}" @end) @indent
9570 "#,
9571 )
9572 .unwrap(),
9573 );
9574
9575 let text = "fn a() {}";
9576
9577 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9578 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9579 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9580 editor
9581 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9582 .await;
9583
9584 editor.update_in(cx, |editor, window, cx| {
9585 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9586 s.select_ranges([5..5, 8..8, 9..9])
9587 });
9588 editor.newline(&Newline, window, cx);
9589 assert_eq!(
9590 editor.text(cx),
9591 indoc!(
9592 "
9593 fn a(
9594
9595 ) {
9596
9597 }
9598 "
9599 )
9600 );
9601 assert_eq!(
9602 editor.selections.ranges(cx),
9603 &[
9604 Point::new(1, 0)..Point::new(1, 0),
9605 Point::new(3, 0)..Point::new(3, 0),
9606 Point::new(5, 0)..Point::new(5, 0)
9607 ]
9608 );
9609 });
9610}
9611
9612#[gpui::test]
9613async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9614 init_test(cx, |settings| {
9615 settings.defaults.auto_indent = Some(true);
9616 settings.languages.0.insert(
9617 "python".into(),
9618 LanguageSettingsContent {
9619 auto_indent: Some(false),
9620 ..Default::default()
9621 },
9622 );
9623 });
9624
9625 let mut cx = EditorTestContext::new(cx).await;
9626
9627 let injected_language = Arc::new(
9628 Language::new(
9629 LanguageConfig {
9630 brackets: BracketPairConfig {
9631 pairs: vec![
9632 BracketPair {
9633 start: "{".to_string(),
9634 end: "}".to_string(),
9635 close: false,
9636 surround: false,
9637 newline: true,
9638 },
9639 BracketPair {
9640 start: "(".to_string(),
9641 end: ")".to_string(),
9642 close: true,
9643 surround: false,
9644 newline: true,
9645 },
9646 ],
9647 ..Default::default()
9648 },
9649 name: "python".into(),
9650 ..Default::default()
9651 },
9652 Some(tree_sitter_python::LANGUAGE.into()),
9653 )
9654 .with_indents_query(
9655 r#"
9656 (_ "(" ")" @end) @indent
9657 (_ "{" "}" @end) @indent
9658 "#,
9659 )
9660 .unwrap(),
9661 );
9662
9663 let language = Arc::new(
9664 Language::new(
9665 LanguageConfig {
9666 brackets: BracketPairConfig {
9667 pairs: vec![
9668 BracketPair {
9669 start: "{".to_string(),
9670 end: "}".to_string(),
9671 close: false,
9672 surround: false,
9673 newline: true,
9674 },
9675 BracketPair {
9676 start: "(".to_string(),
9677 end: ")".to_string(),
9678 close: true,
9679 surround: false,
9680 newline: true,
9681 },
9682 ],
9683 ..Default::default()
9684 },
9685 name: LanguageName::new("rust"),
9686 ..Default::default()
9687 },
9688 Some(tree_sitter_rust::LANGUAGE.into()),
9689 )
9690 .with_indents_query(
9691 r#"
9692 (_ "(" ")" @end) @indent
9693 (_ "{" "}" @end) @indent
9694 "#,
9695 )
9696 .unwrap()
9697 .with_injection_query(
9698 r#"
9699 (macro_invocation
9700 macro: (identifier) @_macro_name
9701 (token_tree) @injection.content
9702 (#set! injection.language "python"))
9703 "#,
9704 )
9705 .unwrap(),
9706 );
9707
9708 cx.language_registry().add(injected_language);
9709 cx.language_registry().add(language.clone());
9710
9711 cx.update_buffer(|buffer, cx| {
9712 buffer.set_language(Some(language), cx);
9713 });
9714
9715 cx.set_state(r#"struct A {ˇ}"#);
9716
9717 cx.update_editor(|editor, window, cx| {
9718 editor.newline(&Default::default(), window, cx);
9719 });
9720
9721 cx.assert_editor_state(indoc!(
9722 "struct A {
9723 ˇ
9724 }"
9725 ));
9726
9727 cx.set_state(r#"select_biased!(ˇ)"#);
9728
9729 cx.update_editor(|editor, window, cx| {
9730 editor.newline(&Default::default(), window, cx);
9731 editor.handle_input("def ", window, cx);
9732 editor.handle_input("(", window, cx);
9733 editor.newline(&Default::default(), window, cx);
9734 editor.handle_input("a", window, cx);
9735 });
9736
9737 cx.assert_editor_state(indoc!(
9738 "select_biased!(
9739 def (
9740 aˇ
9741 )
9742 )"
9743 ));
9744}
9745
9746#[gpui::test]
9747async fn test_autoindent_selections(cx: &mut TestAppContext) {
9748 init_test(cx, |_| {});
9749
9750 {
9751 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9752 cx.set_state(indoc! {"
9753 impl A {
9754
9755 fn b() {}
9756
9757 «fn c() {
9758
9759 }ˇ»
9760 }
9761 "});
9762
9763 cx.update_editor(|editor, window, cx| {
9764 editor.autoindent(&Default::default(), window, cx);
9765 });
9766
9767 cx.assert_editor_state(indoc! {"
9768 impl A {
9769
9770 fn b() {}
9771
9772 «fn c() {
9773
9774 }ˇ»
9775 }
9776 "});
9777 }
9778
9779 {
9780 let mut cx = EditorTestContext::new_multibuffer(
9781 cx,
9782 [indoc! { "
9783 impl A {
9784 «
9785 // a
9786 fn b(){}
9787 »
9788 «
9789 }
9790 fn c(){}
9791 »
9792 "}],
9793 );
9794
9795 let buffer = cx.update_editor(|editor, _, cx| {
9796 let buffer = editor.buffer().update(cx, |buffer, _| {
9797 buffer.all_buffers().iter().next().unwrap().clone()
9798 });
9799 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9800 buffer
9801 });
9802
9803 cx.run_until_parked();
9804 cx.update_editor(|editor, window, cx| {
9805 editor.select_all(&Default::default(), window, cx);
9806 editor.autoindent(&Default::default(), window, cx)
9807 });
9808 cx.run_until_parked();
9809
9810 cx.update(|_, cx| {
9811 assert_eq!(
9812 buffer.read(cx).text(),
9813 indoc! { "
9814 impl A {
9815
9816 // a
9817 fn b(){}
9818
9819
9820 }
9821 fn c(){}
9822
9823 " }
9824 )
9825 });
9826 }
9827}
9828
9829#[gpui::test]
9830async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9831 init_test(cx, |_| {});
9832
9833 let mut cx = EditorTestContext::new(cx).await;
9834
9835 let language = Arc::new(Language::new(
9836 LanguageConfig {
9837 brackets: BracketPairConfig {
9838 pairs: vec![
9839 BracketPair {
9840 start: "{".to_string(),
9841 end: "}".to_string(),
9842 close: true,
9843 surround: true,
9844 newline: true,
9845 },
9846 BracketPair {
9847 start: "(".to_string(),
9848 end: ")".to_string(),
9849 close: true,
9850 surround: true,
9851 newline: true,
9852 },
9853 BracketPair {
9854 start: "/*".to_string(),
9855 end: " */".to_string(),
9856 close: true,
9857 surround: true,
9858 newline: true,
9859 },
9860 BracketPair {
9861 start: "[".to_string(),
9862 end: "]".to_string(),
9863 close: false,
9864 surround: false,
9865 newline: true,
9866 },
9867 BracketPair {
9868 start: "\"".to_string(),
9869 end: "\"".to_string(),
9870 close: true,
9871 surround: true,
9872 newline: false,
9873 },
9874 BracketPair {
9875 start: "<".to_string(),
9876 end: ">".to_string(),
9877 close: false,
9878 surround: true,
9879 newline: true,
9880 },
9881 ],
9882 ..Default::default()
9883 },
9884 autoclose_before: "})]".to_string(),
9885 ..Default::default()
9886 },
9887 Some(tree_sitter_rust::LANGUAGE.into()),
9888 ));
9889
9890 cx.language_registry().add(language.clone());
9891 cx.update_buffer(|buffer, cx| {
9892 buffer.set_language(Some(language), cx);
9893 });
9894
9895 cx.set_state(
9896 &r#"
9897 🏀ˇ
9898 εˇ
9899 ❤️ˇ
9900 "#
9901 .unindent(),
9902 );
9903
9904 // autoclose multiple nested brackets at multiple cursors
9905 cx.update_editor(|editor, window, cx| {
9906 editor.handle_input("{", window, cx);
9907 editor.handle_input("{", window, cx);
9908 editor.handle_input("{", window, cx);
9909 });
9910 cx.assert_editor_state(
9911 &"
9912 🏀{{{ˇ}}}
9913 ε{{{ˇ}}}
9914 ❤️{{{ˇ}}}
9915 "
9916 .unindent(),
9917 );
9918
9919 // insert a different closing bracket
9920 cx.update_editor(|editor, window, cx| {
9921 editor.handle_input(")", window, cx);
9922 });
9923 cx.assert_editor_state(
9924 &"
9925 🏀{{{)ˇ}}}
9926 ε{{{)ˇ}}}
9927 ❤️{{{)ˇ}}}
9928 "
9929 .unindent(),
9930 );
9931
9932 // skip over the auto-closed brackets when typing a closing bracket
9933 cx.update_editor(|editor, window, cx| {
9934 editor.move_right(&MoveRight, window, cx);
9935 editor.handle_input("}", window, cx);
9936 editor.handle_input("}", window, cx);
9937 editor.handle_input("}", window, cx);
9938 });
9939 cx.assert_editor_state(
9940 &"
9941 🏀{{{)}}}}ˇ
9942 ε{{{)}}}}ˇ
9943 ❤️{{{)}}}}ˇ
9944 "
9945 .unindent(),
9946 );
9947
9948 // autoclose multi-character pairs
9949 cx.set_state(
9950 &"
9951 ˇ
9952 ˇ
9953 "
9954 .unindent(),
9955 );
9956 cx.update_editor(|editor, window, cx| {
9957 editor.handle_input("/", window, cx);
9958 editor.handle_input("*", window, cx);
9959 });
9960 cx.assert_editor_state(
9961 &"
9962 /*ˇ */
9963 /*ˇ */
9964 "
9965 .unindent(),
9966 );
9967
9968 // one cursor autocloses a multi-character pair, one cursor
9969 // does not autoclose.
9970 cx.set_state(
9971 &"
9972 /ˇ
9973 ˇ
9974 "
9975 .unindent(),
9976 );
9977 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
9978 cx.assert_editor_state(
9979 &"
9980 /*ˇ */
9981 *ˇ
9982 "
9983 .unindent(),
9984 );
9985
9986 // Don't autoclose if the next character isn't whitespace and isn't
9987 // listed in the language's "autoclose_before" section.
9988 cx.set_state("ˇa b");
9989 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9990 cx.assert_editor_state("{ˇa b");
9991
9992 // Don't autoclose if `close` is false for the bracket pair
9993 cx.set_state("ˇ");
9994 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
9995 cx.assert_editor_state("[ˇ");
9996
9997 // Surround with brackets if text is selected
9998 cx.set_state("«aˇ» b");
9999 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10000 cx.assert_editor_state("{«aˇ»} b");
10001
10002 // Autoclose when not immediately after a word character
10003 cx.set_state("a ˇ");
10004 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10005 cx.assert_editor_state("a \"ˇ\"");
10006
10007 // Autoclose pair where the start and end characters are the same
10008 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10009 cx.assert_editor_state("a \"\"ˇ");
10010
10011 // Don't autoclose when immediately after a word character
10012 cx.set_state("aˇ");
10013 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10014 cx.assert_editor_state("a\"ˇ");
10015
10016 // Do autoclose when after a non-word character
10017 cx.set_state("{ˇ");
10018 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10019 cx.assert_editor_state("{\"ˇ\"");
10020
10021 // Non identical pairs autoclose regardless of preceding character
10022 cx.set_state("aˇ");
10023 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10024 cx.assert_editor_state("a{ˇ}");
10025
10026 // Don't autoclose pair if autoclose is disabled
10027 cx.set_state("ˇ");
10028 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10029 cx.assert_editor_state("<ˇ");
10030
10031 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10032 cx.set_state("«aˇ» b");
10033 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10034 cx.assert_editor_state("<«aˇ»> b");
10035}
10036
10037#[gpui::test]
10038async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10039 init_test(cx, |settings| {
10040 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10041 });
10042
10043 let mut cx = EditorTestContext::new(cx).await;
10044
10045 let language = Arc::new(Language::new(
10046 LanguageConfig {
10047 brackets: BracketPairConfig {
10048 pairs: vec![
10049 BracketPair {
10050 start: "{".to_string(),
10051 end: "}".to_string(),
10052 close: true,
10053 surround: true,
10054 newline: true,
10055 },
10056 BracketPair {
10057 start: "(".to_string(),
10058 end: ")".to_string(),
10059 close: true,
10060 surround: true,
10061 newline: true,
10062 },
10063 BracketPair {
10064 start: "[".to_string(),
10065 end: "]".to_string(),
10066 close: false,
10067 surround: false,
10068 newline: true,
10069 },
10070 ],
10071 ..Default::default()
10072 },
10073 autoclose_before: "})]".to_string(),
10074 ..Default::default()
10075 },
10076 Some(tree_sitter_rust::LANGUAGE.into()),
10077 ));
10078
10079 cx.language_registry().add(language.clone());
10080 cx.update_buffer(|buffer, cx| {
10081 buffer.set_language(Some(language), cx);
10082 });
10083
10084 cx.set_state(
10085 &"
10086 ˇ
10087 ˇ
10088 ˇ
10089 "
10090 .unindent(),
10091 );
10092
10093 // ensure only matching closing brackets are skipped over
10094 cx.update_editor(|editor, window, cx| {
10095 editor.handle_input("}", window, cx);
10096 editor.move_left(&MoveLeft, window, cx);
10097 editor.handle_input(")", window, cx);
10098 editor.move_left(&MoveLeft, window, cx);
10099 });
10100 cx.assert_editor_state(
10101 &"
10102 ˇ)}
10103 ˇ)}
10104 ˇ)}
10105 "
10106 .unindent(),
10107 );
10108
10109 // skip-over closing brackets at multiple cursors
10110 cx.update_editor(|editor, window, cx| {
10111 editor.handle_input(")", window, cx);
10112 editor.handle_input("}", window, cx);
10113 });
10114 cx.assert_editor_state(
10115 &"
10116 )}ˇ
10117 )}ˇ
10118 )}ˇ
10119 "
10120 .unindent(),
10121 );
10122
10123 // ignore non-close brackets
10124 cx.update_editor(|editor, window, cx| {
10125 editor.handle_input("]", window, cx);
10126 editor.move_left(&MoveLeft, window, cx);
10127 editor.handle_input("]", window, cx);
10128 });
10129 cx.assert_editor_state(
10130 &"
10131 )}]ˇ]
10132 )}]ˇ]
10133 )}]ˇ]
10134 "
10135 .unindent(),
10136 );
10137}
10138
10139#[gpui::test]
10140async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10141 init_test(cx, |_| {});
10142
10143 let mut cx = EditorTestContext::new(cx).await;
10144
10145 let html_language = Arc::new(
10146 Language::new(
10147 LanguageConfig {
10148 name: "HTML".into(),
10149 brackets: BracketPairConfig {
10150 pairs: vec![
10151 BracketPair {
10152 start: "<".into(),
10153 end: ">".into(),
10154 close: true,
10155 ..Default::default()
10156 },
10157 BracketPair {
10158 start: "{".into(),
10159 end: "}".into(),
10160 close: true,
10161 ..Default::default()
10162 },
10163 BracketPair {
10164 start: "(".into(),
10165 end: ")".into(),
10166 close: true,
10167 ..Default::default()
10168 },
10169 ],
10170 ..Default::default()
10171 },
10172 autoclose_before: "})]>".into(),
10173 ..Default::default()
10174 },
10175 Some(tree_sitter_html::LANGUAGE.into()),
10176 )
10177 .with_injection_query(
10178 r#"
10179 (script_element
10180 (raw_text) @injection.content
10181 (#set! injection.language "javascript"))
10182 "#,
10183 )
10184 .unwrap(),
10185 );
10186
10187 let javascript_language = Arc::new(Language::new(
10188 LanguageConfig {
10189 name: "JavaScript".into(),
10190 brackets: BracketPairConfig {
10191 pairs: vec![
10192 BracketPair {
10193 start: "/*".into(),
10194 end: " */".into(),
10195 close: true,
10196 ..Default::default()
10197 },
10198 BracketPair {
10199 start: "{".into(),
10200 end: "}".into(),
10201 close: true,
10202 ..Default::default()
10203 },
10204 BracketPair {
10205 start: "(".into(),
10206 end: ")".into(),
10207 close: true,
10208 ..Default::default()
10209 },
10210 ],
10211 ..Default::default()
10212 },
10213 autoclose_before: "})]>".into(),
10214 ..Default::default()
10215 },
10216 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10217 ));
10218
10219 cx.language_registry().add(html_language.clone());
10220 cx.language_registry().add(javascript_language);
10221 cx.executor().run_until_parked();
10222
10223 cx.update_buffer(|buffer, cx| {
10224 buffer.set_language(Some(html_language), cx);
10225 });
10226
10227 cx.set_state(
10228 &r#"
10229 <body>ˇ
10230 <script>
10231 var x = 1;ˇ
10232 </script>
10233 </body>ˇ
10234 "#
10235 .unindent(),
10236 );
10237
10238 // Precondition: different languages are active at different locations.
10239 cx.update_editor(|editor, window, cx| {
10240 let snapshot = editor.snapshot(window, cx);
10241 let cursors = editor.selections.ranges::<usize>(cx);
10242 let languages = cursors
10243 .iter()
10244 .map(|c| snapshot.language_at(c.start).unwrap().name())
10245 .collect::<Vec<_>>();
10246 assert_eq!(
10247 languages,
10248 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10249 );
10250 });
10251
10252 // Angle brackets autoclose in HTML, but not JavaScript.
10253 cx.update_editor(|editor, window, cx| {
10254 editor.handle_input("<", window, cx);
10255 editor.handle_input("a", window, cx);
10256 });
10257 cx.assert_editor_state(
10258 &r#"
10259 <body><aˇ>
10260 <script>
10261 var x = 1;<aˇ
10262 </script>
10263 </body><aˇ>
10264 "#
10265 .unindent(),
10266 );
10267
10268 // Curly braces and parens autoclose in both HTML and JavaScript.
10269 cx.update_editor(|editor, window, cx| {
10270 editor.handle_input(" b=", window, cx);
10271 editor.handle_input("{", window, cx);
10272 editor.handle_input("c", window, cx);
10273 editor.handle_input("(", window, cx);
10274 });
10275 cx.assert_editor_state(
10276 &r#"
10277 <body><a b={c(ˇ)}>
10278 <script>
10279 var x = 1;<a b={c(ˇ)}
10280 </script>
10281 </body><a b={c(ˇ)}>
10282 "#
10283 .unindent(),
10284 );
10285
10286 // Brackets that were already autoclosed are skipped.
10287 cx.update_editor(|editor, window, cx| {
10288 editor.handle_input(")", window, cx);
10289 editor.handle_input("d", window, cx);
10290 editor.handle_input("}", window, cx);
10291 });
10292 cx.assert_editor_state(
10293 &r#"
10294 <body><a b={c()d}ˇ>
10295 <script>
10296 var x = 1;<a b={c()d}ˇ
10297 </script>
10298 </body><a b={c()d}ˇ>
10299 "#
10300 .unindent(),
10301 );
10302 cx.update_editor(|editor, window, cx| {
10303 editor.handle_input(">", window, cx);
10304 });
10305 cx.assert_editor_state(
10306 &r#"
10307 <body><a b={c()d}>ˇ
10308 <script>
10309 var x = 1;<a b={c()d}>ˇ
10310 </script>
10311 </body><a b={c()d}>ˇ
10312 "#
10313 .unindent(),
10314 );
10315
10316 // Reset
10317 cx.set_state(
10318 &r#"
10319 <body>ˇ
10320 <script>
10321 var x = 1;ˇ
10322 </script>
10323 </body>ˇ
10324 "#
10325 .unindent(),
10326 );
10327
10328 cx.update_editor(|editor, window, cx| {
10329 editor.handle_input("<", window, cx);
10330 });
10331 cx.assert_editor_state(
10332 &r#"
10333 <body><ˇ>
10334 <script>
10335 var x = 1;<ˇ
10336 </script>
10337 </body><ˇ>
10338 "#
10339 .unindent(),
10340 );
10341
10342 // When backspacing, the closing angle brackets are removed.
10343 cx.update_editor(|editor, window, cx| {
10344 editor.backspace(&Backspace, window, cx);
10345 });
10346 cx.assert_editor_state(
10347 &r#"
10348 <body>ˇ
10349 <script>
10350 var x = 1;ˇ
10351 </script>
10352 </body>ˇ
10353 "#
10354 .unindent(),
10355 );
10356
10357 // Block comments autoclose in JavaScript, but not HTML.
10358 cx.update_editor(|editor, window, cx| {
10359 editor.handle_input("/", window, cx);
10360 editor.handle_input("*", window, cx);
10361 });
10362 cx.assert_editor_state(
10363 &r#"
10364 <body>/*ˇ
10365 <script>
10366 var x = 1;/*ˇ */
10367 </script>
10368 </body>/*ˇ
10369 "#
10370 .unindent(),
10371 );
10372}
10373
10374#[gpui::test]
10375async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10376 init_test(cx, |_| {});
10377
10378 let mut cx = EditorTestContext::new(cx).await;
10379
10380 let rust_language = Arc::new(
10381 Language::new(
10382 LanguageConfig {
10383 name: "Rust".into(),
10384 brackets: serde_json::from_value(json!([
10385 { "start": "{", "end": "}", "close": true, "newline": true },
10386 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10387 ]))
10388 .unwrap(),
10389 autoclose_before: "})]>".into(),
10390 ..Default::default()
10391 },
10392 Some(tree_sitter_rust::LANGUAGE.into()),
10393 )
10394 .with_override_query("(string_literal) @string")
10395 .unwrap(),
10396 );
10397
10398 cx.language_registry().add(rust_language.clone());
10399 cx.update_buffer(|buffer, cx| {
10400 buffer.set_language(Some(rust_language), cx);
10401 });
10402
10403 cx.set_state(
10404 &r#"
10405 let x = ˇ
10406 "#
10407 .unindent(),
10408 );
10409
10410 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10411 cx.update_editor(|editor, window, cx| {
10412 editor.handle_input("\"", window, cx);
10413 });
10414 cx.assert_editor_state(
10415 &r#"
10416 let x = "ˇ"
10417 "#
10418 .unindent(),
10419 );
10420
10421 // Inserting another quotation mark. The cursor moves across the existing
10422 // automatically-inserted quotation mark.
10423 cx.update_editor(|editor, window, cx| {
10424 editor.handle_input("\"", window, cx);
10425 });
10426 cx.assert_editor_state(
10427 &r#"
10428 let x = ""ˇ
10429 "#
10430 .unindent(),
10431 );
10432
10433 // Reset
10434 cx.set_state(
10435 &r#"
10436 let x = ˇ
10437 "#
10438 .unindent(),
10439 );
10440
10441 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10442 cx.update_editor(|editor, window, cx| {
10443 editor.handle_input("\"", window, cx);
10444 editor.handle_input(" ", window, cx);
10445 editor.move_left(&Default::default(), window, cx);
10446 editor.handle_input("\\", window, cx);
10447 editor.handle_input("\"", window, cx);
10448 });
10449 cx.assert_editor_state(
10450 &r#"
10451 let x = "\"ˇ "
10452 "#
10453 .unindent(),
10454 );
10455
10456 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10457 // mark. Nothing is inserted.
10458 cx.update_editor(|editor, window, cx| {
10459 editor.move_right(&Default::default(), window, cx);
10460 editor.handle_input("\"", window, cx);
10461 });
10462 cx.assert_editor_state(
10463 &r#"
10464 let x = "\" "ˇ
10465 "#
10466 .unindent(),
10467 );
10468}
10469
10470#[gpui::test]
10471async fn test_surround_with_pair(cx: &mut TestAppContext) {
10472 init_test(cx, |_| {});
10473
10474 let language = Arc::new(Language::new(
10475 LanguageConfig {
10476 brackets: BracketPairConfig {
10477 pairs: vec![
10478 BracketPair {
10479 start: "{".to_string(),
10480 end: "}".to_string(),
10481 close: true,
10482 surround: true,
10483 newline: true,
10484 },
10485 BracketPair {
10486 start: "/* ".to_string(),
10487 end: "*/".to_string(),
10488 close: true,
10489 surround: true,
10490 ..Default::default()
10491 },
10492 ],
10493 ..Default::default()
10494 },
10495 ..Default::default()
10496 },
10497 Some(tree_sitter_rust::LANGUAGE.into()),
10498 ));
10499
10500 let text = r#"
10501 a
10502 b
10503 c
10504 "#
10505 .unindent();
10506
10507 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10508 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10509 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10510 editor
10511 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10512 .await;
10513
10514 editor.update_in(cx, |editor, window, cx| {
10515 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10516 s.select_display_ranges([
10517 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10518 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10519 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10520 ])
10521 });
10522
10523 editor.handle_input("{", window, cx);
10524 editor.handle_input("{", window, cx);
10525 editor.handle_input("{", window, cx);
10526 assert_eq!(
10527 editor.text(cx),
10528 "
10529 {{{a}}}
10530 {{{b}}}
10531 {{{c}}}
10532 "
10533 .unindent()
10534 );
10535 assert_eq!(
10536 editor.selections.display_ranges(cx),
10537 [
10538 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10539 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10540 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10541 ]
10542 );
10543
10544 editor.undo(&Undo, window, cx);
10545 editor.undo(&Undo, window, cx);
10546 editor.undo(&Undo, window, cx);
10547 assert_eq!(
10548 editor.text(cx),
10549 "
10550 a
10551 b
10552 c
10553 "
10554 .unindent()
10555 );
10556 assert_eq!(
10557 editor.selections.display_ranges(cx),
10558 [
10559 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10560 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10561 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10562 ]
10563 );
10564
10565 // Ensure inserting the first character of a multi-byte bracket pair
10566 // doesn't surround the selections with the bracket.
10567 editor.handle_input("/", window, cx);
10568 assert_eq!(
10569 editor.text(cx),
10570 "
10571 /
10572 /
10573 /
10574 "
10575 .unindent()
10576 );
10577 assert_eq!(
10578 editor.selections.display_ranges(cx),
10579 [
10580 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10581 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10582 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10583 ]
10584 );
10585
10586 editor.undo(&Undo, window, cx);
10587 assert_eq!(
10588 editor.text(cx),
10589 "
10590 a
10591 b
10592 c
10593 "
10594 .unindent()
10595 );
10596 assert_eq!(
10597 editor.selections.display_ranges(cx),
10598 [
10599 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10600 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10601 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10602 ]
10603 );
10604
10605 // Ensure inserting the last character of a multi-byte bracket pair
10606 // doesn't surround the selections with the bracket.
10607 editor.handle_input("*", window, cx);
10608 assert_eq!(
10609 editor.text(cx),
10610 "
10611 *
10612 *
10613 *
10614 "
10615 .unindent()
10616 );
10617 assert_eq!(
10618 editor.selections.display_ranges(cx),
10619 [
10620 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10621 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10622 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10623 ]
10624 );
10625 });
10626}
10627
10628#[gpui::test]
10629async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10630 init_test(cx, |_| {});
10631
10632 let language = Arc::new(Language::new(
10633 LanguageConfig {
10634 brackets: BracketPairConfig {
10635 pairs: vec![BracketPair {
10636 start: "{".to_string(),
10637 end: "}".to_string(),
10638 close: true,
10639 surround: true,
10640 newline: true,
10641 }],
10642 ..Default::default()
10643 },
10644 autoclose_before: "}".to_string(),
10645 ..Default::default()
10646 },
10647 Some(tree_sitter_rust::LANGUAGE.into()),
10648 ));
10649
10650 let text = r#"
10651 a
10652 b
10653 c
10654 "#
10655 .unindent();
10656
10657 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10658 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10659 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10660 editor
10661 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10662 .await;
10663
10664 editor.update_in(cx, |editor, window, cx| {
10665 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10666 s.select_ranges([
10667 Point::new(0, 1)..Point::new(0, 1),
10668 Point::new(1, 1)..Point::new(1, 1),
10669 Point::new(2, 1)..Point::new(2, 1),
10670 ])
10671 });
10672
10673 editor.handle_input("{", window, cx);
10674 editor.handle_input("{", window, cx);
10675 editor.handle_input("_", window, cx);
10676 assert_eq!(
10677 editor.text(cx),
10678 "
10679 a{{_}}
10680 b{{_}}
10681 c{{_}}
10682 "
10683 .unindent()
10684 );
10685 assert_eq!(
10686 editor.selections.ranges::<Point>(cx),
10687 [
10688 Point::new(0, 4)..Point::new(0, 4),
10689 Point::new(1, 4)..Point::new(1, 4),
10690 Point::new(2, 4)..Point::new(2, 4)
10691 ]
10692 );
10693
10694 editor.backspace(&Default::default(), window, cx);
10695 editor.backspace(&Default::default(), window, cx);
10696 assert_eq!(
10697 editor.text(cx),
10698 "
10699 a{}
10700 b{}
10701 c{}
10702 "
10703 .unindent()
10704 );
10705 assert_eq!(
10706 editor.selections.ranges::<Point>(cx),
10707 [
10708 Point::new(0, 2)..Point::new(0, 2),
10709 Point::new(1, 2)..Point::new(1, 2),
10710 Point::new(2, 2)..Point::new(2, 2)
10711 ]
10712 );
10713
10714 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10715 assert_eq!(
10716 editor.text(cx),
10717 "
10718 a
10719 b
10720 c
10721 "
10722 .unindent()
10723 );
10724 assert_eq!(
10725 editor.selections.ranges::<Point>(cx),
10726 [
10727 Point::new(0, 1)..Point::new(0, 1),
10728 Point::new(1, 1)..Point::new(1, 1),
10729 Point::new(2, 1)..Point::new(2, 1)
10730 ]
10731 );
10732 });
10733}
10734
10735#[gpui::test]
10736async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10737 init_test(cx, |settings| {
10738 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10739 });
10740
10741 let mut cx = EditorTestContext::new(cx).await;
10742
10743 let language = Arc::new(Language::new(
10744 LanguageConfig {
10745 brackets: BracketPairConfig {
10746 pairs: vec![
10747 BracketPair {
10748 start: "{".to_string(),
10749 end: "}".to_string(),
10750 close: true,
10751 surround: true,
10752 newline: true,
10753 },
10754 BracketPair {
10755 start: "(".to_string(),
10756 end: ")".to_string(),
10757 close: true,
10758 surround: true,
10759 newline: true,
10760 },
10761 BracketPair {
10762 start: "[".to_string(),
10763 end: "]".to_string(),
10764 close: false,
10765 surround: true,
10766 newline: true,
10767 },
10768 ],
10769 ..Default::default()
10770 },
10771 autoclose_before: "})]".to_string(),
10772 ..Default::default()
10773 },
10774 Some(tree_sitter_rust::LANGUAGE.into()),
10775 ));
10776
10777 cx.language_registry().add(language.clone());
10778 cx.update_buffer(|buffer, cx| {
10779 buffer.set_language(Some(language), cx);
10780 });
10781
10782 cx.set_state(
10783 &"
10784 {(ˇ)}
10785 [[ˇ]]
10786 {(ˇ)}
10787 "
10788 .unindent(),
10789 );
10790
10791 cx.update_editor(|editor, window, cx| {
10792 editor.backspace(&Default::default(), window, cx);
10793 editor.backspace(&Default::default(), window, cx);
10794 });
10795
10796 cx.assert_editor_state(
10797 &"
10798 ˇ
10799 ˇ]]
10800 ˇ
10801 "
10802 .unindent(),
10803 );
10804
10805 cx.update_editor(|editor, window, cx| {
10806 editor.handle_input("{", window, cx);
10807 editor.handle_input("{", window, cx);
10808 editor.move_right(&MoveRight, window, cx);
10809 editor.move_right(&MoveRight, window, cx);
10810 editor.move_left(&MoveLeft, window, cx);
10811 editor.move_left(&MoveLeft, window, cx);
10812 editor.backspace(&Default::default(), window, cx);
10813 });
10814
10815 cx.assert_editor_state(
10816 &"
10817 {ˇ}
10818 {ˇ}]]
10819 {ˇ}
10820 "
10821 .unindent(),
10822 );
10823
10824 cx.update_editor(|editor, window, cx| {
10825 editor.backspace(&Default::default(), window, cx);
10826 });
10827
10828 cx.assert_editor_state(
10829 &"
10830 ˇ
10831 ˇ]]
10832 ˇ
10833 "
10834 .unindent(),
10835 );
10836}
10837
10838#[gpui::test]
10839async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10840 init_test(cx, |_| {});
10841
10842 let language = Arc::new(Language::new(
10843 LanguageConfig::default(),
10844 Some(tree_sitter_rust::LANGUAGE.into()),
10845 ));
10846
10847 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10848 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10849 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10850 editor
10851 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10852 .await;
10853
10854 editor.update_in(cx, |editor, window, cx| {
10855 editor.set_auto_replace_emoji_shortcode(true);
10856
10857 editor.handle_input("Hello ", window, cx);
10858 editor.handle_input(":wave", window, cx);
10859 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10860
10861 editor.handle_input(":", window, cx);
10862 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10863
10864 editor.handle_input(" :smile", window, cx);
10865 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10866
10867 editor.handle_input(":", window, cx);
10868 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10869
10870 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10871 editor.handle_input(":wave", window, cx);
10872 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10873
10874 editor.handle_input(":", window, cx);
10875 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10876
10877 editor.handle_input(":1", window, cx);
10878 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10879
10880 editor.handle_input(":", window, cx);
10881 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10882
10883 // Ensure shortcode does not get replaced when it is part of a word
10884 editor.handle_input(" Test:wave", window, cx);
10885 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
10886
10887 editor.handle_input(":", window, cx);
10888 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
10889
10890 editor.set_auto_replace_emoji_shortcode(false);
10891
10892 // Ensure shortcode does not get replaced when auto replace is off
10893 editor.handle_input(" :wave", window, cx);
10894 assert_eq!(
10895 editor.text(cx),
10896 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
10897 );
10898
10899 editor.handle_input(":", window, cx);
10900 assert_eq!(
10901 editor.text(cx),
10902 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
10903 );
10904 });
10905}
10906
10907#[gpui::test]
10908async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
10909 init_test(cx, |_| {});
10910
10911 let (text, insertion_ranges) = marked_text_ranges(
10912 indoc! {"
10913 ˇ
10914 "},
10915 false,
10916 );
10917
10918 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
10919 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10920
10921 _ = editor.update_in(cx, |editor, window, cx| {
10922 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
10923
10924 editor
10925 .insert_snippet(&insertion_ranges, snippet, window, cx)
10926 .unwrap();
10927
10928 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
10929 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
10930 assert_eq!(editor.text(cx), expected_text);
10931 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
10932 }
10933
10934 assert(
10935 editor,
10936 cx,
10937 indoc! {"
10938 type «» =•
10939 "},
10940 );
10941
10942 assert!(editor.context_menu_visible(), "There should be a matches");
10943 });
10944}
10945
10946#[gpui::test]
10947async fn test_snippets(cx: &mut TestAppContext) {
10948 init_test(cx, |_| {});
10949
10950 let mut cx = EditorTestContext::new(cx).await;
10951
10952 cx.set_state(indoc! {"
10953 a.ˇ b
10954 a.ˇ b
10955 a.ˇ b
10956 "});
10957
10958 cx.update_editor(|editor, window, cx| {
10959 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
10960 let insertion_ranges = editor
10961 .selections
10962 .all(cx)
10963 .iter()
10964 .map(|s| s.range())
10965 .collect::<Vec<_>>();
10966 editor
10967 .insert_snippet(&insertion_ranges, snippet, window, cx)
10968 .unwrap();
10969 });
10970
10971 cx.assert_editor_state(indoc! {"
10972 a.f(«oneˇ», two, «threeˇ») b
10973 a.f(«oneˇ», two, «threeˇ») b
10974 a.f(«oneˇ», two, «threeˇ») b
10975 "});
10976
10977 // Can't move earlier than the first tab stop
10978 cx.update_editor(|editor, window, cx| {
10979 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10980 });
10981 cx.assert_editor_state(indoc! {"
10982 a.f(«oneˇ», two, «threeˇ») b
10983 a.f(«oneˇ», two, «threeˇ») b
10984 a.f(«oneˇ», two, «threeˇ») b
10985 "});
10986
10987 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
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 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
10995 cx.assert_editor_state(indoc! {"
10996 a.f(«oneˇ», two, «threeˇ») b
10997 a.f(«oneˇ», two, «threeˇ») b
10998 a.f(«oneˇ», two, «threeˇ») b
10999 "});
11000
11001 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11002 cx.assert_editor_state(indoc! {"
11003 a.f(one, «twoˇ», three) b
11004 a.f(one, «twoˇ», three) b
11005 a.f(one, «twoˇ», three) b
11006 "});
11007 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11008 cx.assert_editor_state(indoc! {"
11009 a.f(one, two, three)ˇ b
11010 a.f(one, two, three)ˇ b
11011 a.f(one, two, three)ˇ b
11012 "});
11013
11014 // As soon as the last tab stop is reached, snippet state is gone
11015 cx.update_editor(|editor, window, cx| {
11016 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11017 });
11018 cx.assert_editor_state(indoc! {"
11019 a.f(one, two, three)ˇ b
11020 a.f(one, two, three)ˇ b
11021 a.f(one, two, three)ˇ b
11022 "});
11023}
11024
11025#[gpui::test]
11026async fn test_snippet_indentation(cx: &mut TestAppContext) {
11027 init_test(cx, |_| {});
11028
11029 let mut cx = EditorTestContext::new(cx).await;
11030
11031 cx.update_editor(|editor, window, cx| {
11032 let snippet = Snippet::parse(indoc! {"
11033 /*
11034 * Multiline comment with leading indentation
11035 *
11036 * $1
11037 */
11038 $0"})
11039 .unwrap();
11040 let insertion_ranges = editor
11041 .selections
11042 .all(cx)
11043 .iter()
11044 .map(|s| s.range())
11045 .collect::<Vec<_>>();
11046 editor
11047 .insert_snippet(&insertion_ranges, snippet, window, cx)
11048 .unwrap();
11049 });
11050
11051 cx.assert_editor_state(indoc! {"
11052 /*
11053 * Multiline comment with leading indentation
11054 *
11055 * ˇ
11056 */
11057 "});
11058
11059 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11060 cx.assert_editor_state(indoc! {"
11061 /*
11062 * Multiline comment with leading indentation
11063 *
11064 *•
11065 */
11066 ˇ"});
11067}
11068
11069#[gpui::test]
11070async fn test_document_format_during_save(cx: &mut TestAppContext) {
11071 init_test(cx, |_| {});
11072
11073 let fs = FakeFs::new(cx.executor());
11074 fs.insert_file(path!("/file.rs"), Default::default()).await;
11075
11076 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11077
11078 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11079 language_registry.add(rust_lang());
11080 let mut fake_servers = language_registry.register_fake_lsp(
11081 "Rust",
11082 FakeLspAdapter {
11083 capabilities: lsp::ServerCapabilities {
11084 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11085 ..Default::default()
11086 },
11087 ..Default::default()
11088 },
11089 );
11090
11091 let buffer = project
11092 .update(cx, |project, cx| {
11093 project.open_local_buffer(path!("/file.rs"), cx)
11094 })
11095 .await
11096 .unwrap();
11097
11098 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11099 let (editor, cx) = cx.add_window_view(|window, cx| {
11100 build_editor_with_project(project.clone(), buffer, window, cx)
11101 });
11102 editor.update_in(cx, |editor, window, cx| {
11103 editor.set_text("one\ntwo\nthree\n", window, cx)
11104 });
11105 assert!(cx.read(|cx| editor.is_dirty(cx)));
11106
11107 cx.executor().start_waiting();
11108 let fake_server = fake_servers.next().await.unwrap();
11109
11110 {
11111 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11112 move |params, _| async move {
11113 assert_eq!(
11114 params.text_document.uri,
11115 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11116 );
11117 assert_eq!(params.options.tab_size, 4);
11118 Ok(Some(vec![lsp::TextEdit::new(
11119 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11120 ", ".to_string(),
11121 )]))
11122 },
11123 );
11124 let save = editor
11125 .update_in(cx, |editor, window, cx| {
11126 editor.save(
11127 SaveOptions {
11128 format: true,
11129 autosave: false,
11130 },
11131 project.clone(),
11132 window,
11133 cx,
11134 )
11135 })
11136 .unwrap();
11137 cx.executor().start_waiting();
11138 save.await;
11139
11140 assert_eq!(
11141 editor.update(cx, |editor, cx| editor.text(cx)),
11142 "one, two\nthree\n"
11143 );
11144 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11145 }
11146
11147 {
11148 editor.update_in(cx, |editor, window, cx| {
11149 editor.set_text("one\ntwo\nthree\n", window, cx)
11150 });
11151 assert!(cx.read(|cx| editor.is_dirty(cx)));
11152
11153 // Ensure we can still save even if formatting hangs.
11154 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11155 move |params, _| async move {
11156 assert_eq!(
11157 params.text_document.uri,
11158 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11159 );
11160 futures::future::pending::<()>().await;
11161 unreachable!()
11162 },
11163 );
11164 let save = editor
11165 .update_in(cx, |editor, window, cx| {
11166 editor.save(
11167 SaveOptions {
11168 format: true,
11169 autosave: false,
11170 },
11171 project.clone(),
11172 window,
11173 cx,
11174 )
11175 })
11176 .unwrap();
11177 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11178 cx.executor().start_waiting();
11179 save.await;
11180 assert_eq!(
11181 editor.update(cx, |editor, cx| editor.text(cx)),
11182 "one\ntwo\nthree\n"
11183 );
11184 }
11185
11186 // Set rust language override and assert overridden tabsize is sent to language server
11187 update_test_language_settings(cx, |settings| {
11188 settings.languages.0.insert(
11189 "Rust".into(),
11190 LanguageSettingsContent {
11191 tab_size: NonZeroU32::new(8),
11192 ..Default::default()
11193 },
11194 );
11195 });
11196
11197 {
11198 editor.update_in(cx, |editor, window, cx| {
11199 editor.set_text("somehting_new\n", window, cx)
11200 });
11201 assert!(cx.read(|cx| editor.is_dirty(cx)));
11202 let _formatting_request_signal = fake_server
11203 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11204 assert_eq!(
11205 params.text_document.uri,
11206 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11207 );
11208 assert_eq!(params.options.tab_size, 8);
11209 Ok(Some(vec![]))
11210 });
11211 let save = editor
11212 .update_in(cx, |editor, window, cx| {
11213 editor.save(
11214 SaveOptions {
11215 format: true,
11216 autosave: false,
11217 },
11218 project.clone(),
11219 window,
11220 cx,
11221 )
11222 })
11223 .unwrap();
11224 cx.executor().start_waiting();
11225 save.await;
11226 }
11227}
11228
11229#[gpui::test]
11230async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11231 init_test(cx, |settings| {
11232 settings.defaults.ensure_final_newline_on_save = Some(false);
11233 });
11234
11235 let fs = FakeFs::new(cx.executor());
11236 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11237
11238 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11239
11240 let buffer = project
11241 .update(cx, |project, cx| {
11242 project.open_local_buffer(path!("/file.txt"), cx)
11243 })
11244 .await
11245 .unwrap();
11246
11247 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11248 let (editor, cx) = cx.add_window_view(|window, cx| {
11249 build_editor_with_project(project.clone(), buffer, window, cx)
11250 });
11251 editor.update_in(cx, |editor, window, cx| {
11252 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11253 s.select_ranges([0..0])
11254 });
11255 });
11256 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11257
11258 editor.update_in(cx, |editor, window, cx| {
11259 editor.handle_input("\n", window, cx)
11260 });
11261 cx.run_until_parked();
11262 save(&editor, &project, cx).await;
11263 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11264
11265 editor.update_in(cx, |editor, window, cx| {
11266 editor.undo(&Default::default(), window, cx);
11267 });
11268 save(&editor, &project, cx).await;
11269 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11270
11271 editor.update_in(cx, |editor, window, cx| {
11272 editor.redo(&Default::default(), window, cx);
11273 });
11274 cx.run_until_parked();
11275 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11276
11277 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11278 let save = editor
11279 .update_in(cx, |editor, window, cx| {
11280 editor.save(
11281 SaveOptions {
11282 format: true,
11283 autosave: false,
11284 },
11285 project.clone(),
11286 window,
11287 cx,
11288 )
11289 })
11290 .unwrap();
11291 cx.executor().start_waiting();
11292 save.await;
11293 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11294 }
11295}
11296
11297#[gpui::test]
11298async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11299 init_test(cx, |_| {});
11300
11301 let cols = 4;
11302 let rows = 10;
11303 let sample_text_1 = sample_text(rows, cols, 'a');
11304 assert_eq!(
11305 sample_text_1,
11306 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11307 );
11308 let sample_text_2 = sample_text(rows, cols, 'l');
11309 assert_eq!(
11310 sample_text_2,
11311 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11312 );
11313 let sample_text_3 = sample_text(rows, cols, 'v');
11314 assert_eq!(
11315 sample_text_3,
11316 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11317 );
11318
11319 let fs = FakeFs::new(cx.executor());
11320 fs.insert_tree(
11321 path!("/a"),
11322 json!({
11323 "main.rs": sample_text_1,
11324 "other.rs": sample_text_2,
11325 "lib.rs": sample_text_3,
11326 }),
11327 )
11328 .await;
11329
11330 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11331 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11332 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11333
11334 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11335 language_registry.add(rust_lang());
11336 let mut fake_servers = language_registry.register_fake_lsp(
11337 "Rust",
11338 FakeLspAdapter {
11339 capabilities: lsp::ServerCapabilities {
11340 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11341 ..Default::default()
11342 },
11343 ..Default::default()
11344 },
11345 );
11346
11347 let worktree = project.update(cx, |project, cx| {
11348 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11349 assert_eq!(worktrees.len(), 1);
11350 worktrees.pop().unwrap()
11351 });
11352 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11353
11354 let buffer_1 = project
11355 .update(cx, |project, cx| {
11356 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11357 })
11358 .await
11359 .unwrap();
11360 let buffer_2 = project
11361 .update(cx, |project, cx| {
11362 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11363 })
11364 .await
11365 .unwrap();
11366 let buffer_3 = project
11367 .update(cx, |project, cx| {
11368 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11369 })
11370 .await
11371 .unwrap();
11372
11373 let multi_buffer = cx.new(|cx| {
11374 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11375 multi_buffer.push_excerpts(
11376 buffer_1.clone(),
11377 [
11378 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11379 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11380 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11381 ],
11382 cx,
11383 );
11384 multi_buffer.push_excerpts(
11385 buffer_2.clone(),
11386 [
11387 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11388 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11389 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11390 ],
11391 cx,
11392 );
11393 multi_buffer.push_excerpts(
11394 buffer_3.clone(),
11395 [
11396 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11397 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11398 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11399 ],
11400 cx,
11401 );
11402 multi_buffer
11403 });
11404 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11405 Editor::new(
11406 EditorMode::full(),
11407 multi_buffer,
11408 Some(project.clone()),
11409 window,
11410 cx,
11411 )
11412 });
11413
11414 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11415 editor.change_selections(
11416 SelectionEffects::scroll(Autoscroll::Next),
11417 window,
11418 cx,
11419 |s| s.select_ranges(Some(1..2)),
11420 );
11421 editor.insert("|one|two|three|", window, cx);
11422 });
11423 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11424 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11425 editor.change_selections(
11426 SelectionEffects::scroll(Autoscroll::Next),
11427 window,
11428 cx,
11429 |s| s.select_ranges(Some(60..70)),
11430 );
11431 editor.insert("|four|five|six|", window, cx);
11432 });
11433 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11434
11435 // First two buffers should be edited, but not the third one.
11436 assert_eq!(
11437 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11438 "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}",
11439 );
11440 buffer_1.update(cx, |buffer, _| {
11441 assert!(buffer.is_dirty());
11442 assert_eq!(
11443 buffer.text(),
11444 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11445 )
11446 });
11447 buffer_2.update(cx, |buffer, _| {
11448 assert!(buffer.is_dirty());
11449 assert_eq!(
11450 buffer.text(),
11451 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11452 )
11453 });
11454 buffer_3.update(cx, |buffer, _| {
11455 assert!(!buffer.is_dirty());
11456 assert_eq!(buffer.text(), sample_text_3,)
11457 });
11458 cx.executor().run_until_parked();
11459
11460 cx.executor().start_waiting();
11461 let save = multi_buffer_editor
11462 .update_in(cx, |editor, window, cx| {
11463 editor.save(
11464 SaveOptions {
11465 format: true,
11466 autosave: false,
11467 },
11468 project.clone(),
11469 window,
11470 cx,
11471 )
11472 })
11473 .unwrap();
11474
11475 let fake_server = fake_servers.next().await.unwrap();
11476 fake_server
11477 .server
11478 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11479 Ok(Some(vec![lsp::TextEdit::new(
11480 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11481 format!("[{} formatted]", params.text_document.uri),
11482 )]))
11483 })
11484 .detach();
11485 save.await;
11486
11487 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11488 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11489 assert_eq!(
11490 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11491 uri!(
11492 "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}"
11493 ),
11494 );
11495 buffer_1.update(cx, |buffer, _| {
11496 assert!(!buffer.is_dirty());
11497 assert_eq!(
11498 buffer.text(),
11499 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11500 )
11501 });
11502 buffer_2.update(cx, |buffer, _| {
11503 assert!(!buffer.is_dirty());
11504 assert_eq!(
11505 buffer.text(),
11506 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11507 )
11508 });
11509 buffer_3.update(cx, |buffer, _| {
11510 assert!(!buffer.is_dirty());
11511 assert_eq!(buffer.text(), sample_text_3,)
11512 });
11513}
11514
11515#[gpui::test]
11516async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11517 init_test(cx, |_| {});
11518
11519 let fs = FakeFs::new(cx.executor());
11520 fs.insert_tree(
11521 path!("/dir"),
11522 json!({
11523 "file1.rs": "fn main() { println!(\"hello\"); }",
11524 "file2.rs": "fn test() { println!(\"test\"); }",
11525 "file3.rs": "fn other() { println!(\"other\"); }\n",
11526 }),
11527 )
11528 .await;
11529
11530 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11531 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11532 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11533
11534 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11535 language_registry.add(rust_lang());
11536
11537 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11538 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11539
11540 // Open three buffers
11541 let buffer_1 = project
11542 .update(cx, |project, cx| {
11543 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11544 })
11545 .await
11546 .unwrap();
11547 let buffer_2 = project
11548 .update(cx, |project, cx| {
11549 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11550 })
11551 .await
11552 .unwrap();
11553 let buffer_3 = project
11554 .update(cx, |project, cx| {
11555 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11556 })
11557 .await
11558 .unwrap();
11559
11560 // Create a multi-buffer with all three buffers
11561 let multi_buffer = cx.new(|cx| {
11562 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11563 multi_buffer.push_excerpts(
11564 buffer_1.clone(),
11565 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11566 cx,
11567 );
11568 multi_buffer.push_excerpts(
11569 buffer_2.clone(),
11570 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11571 cx,
11572 );
11573 multi_buffer.push_excerpts(
11574 buffer_3.clone(),
11575 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11576 cx,
11577 );
11578 multi_buffer
11579 });
11580
11581 let editor = cx.new_window_entity(|window, cx| {
11582 Editor::new(
11583 EditorMode::full(),
11584 multi_buffer,
11585 Some(project.clone()),
11586 window,
11587 cx,
11588 )
11589 });
11590
11591 // Edit only the first buffer
11592 editor.update_in(cx, |editor, window, cx| {
11593 editor.change_selections(
11594 SelectionEffects::scroll(Autoscroll::Next),
11595 window,
11596 cx,
11597 |s| s.select_ranges(Some(10..10)),
11598 );
11599 editor.insert("// edited", window, cx);
11600 });
11601
11602 // Verify that only buffer 1 is dirty
11603 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11604 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11605 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11606
11607 // Get write counts after file creation (files were created with initial content)
11608 // We expect each file to have been written once during creation
11609 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11610 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11611 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11612
11613 // Perform autosave
11614 let save_task = editor.update_in(cx, |editor, window, cx| {
11615 editor.save(
11616 SaveOptions {
11617 format: true,
11618 autosave: true,
11619 },
11620 project.clone(),
11621 window,
11622 cx,
11623 )
11624 });
11625 save_task.await.unwrap();
11626
11627 // Only the dirty buffer should have been saved
11628 assert_eq!(
11629 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11630 1,
11631 "Buffer 1 was dirty, so it should have been written once during autosave"
11632 );
11633 assert_eq!(
11634 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11635 0,
11636 "Buffer 2 was clean, so it should not have been written during autosave"
11637 );
11638 assert_eq!(
11639 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11640 0,
11641 "Buffer 3 was clean, so it should not have been written during autosave"
11642 );
11643
11644 // Verify buffer states after autosave
11645 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11646 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11647 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11648
11649 // Now perform a manual save (format = true)
11650 let save_task = editor.update_in(cx, |editor, window, cx| {
11651 editor.save(
11652 SaveOptions {
11653 format: true,
11654 autosave: false,
11655 },
11656 project.clone(),
11657 window,
11658 cx,
11659 )
11660 });
11661 save_task.await.unwrap();
11662
11663 // During manual save, clean buffers don't get written to disk
11664 // They just get did_save called for language server notifications
11665 assert_eq!(
11666 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11667 1,
11668 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11669 );
11670 assert_eq!(
11671 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11672 0,
11673 "Buffer 2 should not have been written at all"
11674 );
11675 assert_eq!(
11676 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11677 0,
11678 "Buffer 3 should not have been written at all"
11679 );
11680}
11681
11682async fn setup_range_format_test(
11683 cx: &mut TestAppContext,
11684) -> (
11685 Entity<Project>,
11686 Entity<Editor>,
11687 &mut gpui::VisualTestContext,
11688 lsp::FakeLanguageServer,
11689) {
11690 init_test(cx, |_| {});
11691
11692 let fs = FakeFs::new(cx.executor());
11693 fs.insert_file(path!("/file.rs"), Default::default()).await;
11694
11695 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11696
11697 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11698 language_registry.add(rust_lang());
11699 let mut fake_servers = language_registry.register_fake_lsp(
11700 "Rust",
11701 FakeLspAdapter {
11702 capabilities: lsp::ServerCapabilities {
11703 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11704 ..lsp::ServerCapabilities::default()
11705 },
11706 ..FakeLspAdapter::default()
11707 },
11708 );
11709
11710 let buffer = project
11711 .update(cx, |project, cx| {
11712 project.open_local_buffer(path!("/file.rs"), cx)
11713 })
11714 .await
11715 .unwrap();
11716
11717 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11718 let (editor, cx) = cx.add_window_view(|window, cx| {
11719 build_editor_with_project(project.clone(), buffer, window, cx)
11720 });
11721
11722 cx.executor().start_waiting();
11723 let fake_server = fake_servers.next().await.unwrap();
11724
11725 (project, editor, cx, fake_server)
11726}
11727
11728#[gpui::test]
11729async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11730 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11731
11732 editor.update_in(cx, |editor, window, cx| {
11733 editor.set_text("one\ntwo\nthree\n", window, cx)
11734 });
11735 assert!(cx.read(|cx| editor.is_dirty(cx)));
11736
11737 let save = editor
11738 .update_in(cx, |editor, window, cx| {
11739 editor.save(
11740 SaveOptions {
11741 format: true,
11742 autosave: false,
11743 },
11744 project.clone(),
11745 window,
11746 cx,
11747 )
11748 })
11749 .unwrap();
11750 fake_server
11751 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11752 assert_eq!(
11753 params.text_document.uri,
11754 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11755 );
11756 assert_eq!(params.options.tab_size, 4);
11757 Ok(Some(vec![lsp::TextEdit::new(
11758 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11759 ", ".to_string(),
11760 )]))
11761 })
11762 .next()
11763 .await;
11764 cx.executor().start_waiting();
11765 save.await;
11766 assert_eq!(
11767 editor.update(cx, |editor, cx| editor.text(cx)),
11768 "one, two\nthree\n"
11769 );
11770 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11771}
11772
11773#[gpui::test]
11774async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11775 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11776
11777 editor.update_in(cx, |editor, window, cx| {
11778 editor.set_text("one\ntwo\nthree\n", window, cx)
11779 });
11780 assert!(cx.read(|cx| editor.is_dirty(cx)));
11781
11782 // Test that save still works when formatting hangs
11783 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11784 move |params, _| async move {
11785 assert_eq!(
11786 params.text_document.uri,
11787 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11788 );
11789 futures::future::pending::<()>().await;
11790 unreachable!()
11791 },
11792 );
11793 let save = editor
11794 .update_in(cx, |editor, window, cx| {
11795 editor.save(
11796 SaveOptions {
11797 format: true,
11798 autosave: false,
11799 },
11800 project.clone(),
11801 window,
11802 cx,
11803 )
11804 })
11805 .unwrap();
11806 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11807 cx.executor().start_waiting();
11808 save.await;
11809 assert_eq!(
11810 editor.update(cx, |editor, cx| editor.text(cx)),
11811 "one\ntwo\nthree\n"
11812 );
11813 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11814}
11815
11816#[gpui::test]
11817async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11818 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11819
11820 // Buffer starts clean, no formatting should be requested
11821 let save = editor
11822 .update_in(cx, |editor, window, cx| {
11823 editor.save(
11824 SaveOptions {
11825 format: false,
11826 autosave: false,
11827 },
11828 project.clone(),
11829 window,
11830 cx,
11831 )
11832 })
11833 .unwrap();
11834 let _pending_format_request = fake_server
11835 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11836 panic!("Should not be invoked");
11837 })
11838 .next();
11839 cx.executor().start_waiting();
11840 save.await;
11841 cx.run_until_parked();
11842}
11843
11844#[gpui::test]
11845async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11846 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11847
11848 // Set Rust language override and assert overridden tabsize is sent to language server
11849 update_test_language_settings(cx, |settings| {
11850 settings.languages.0.insert(
11851 "Rust".into(),
11852 LanguageSettingsContent {
11853 tab_size: NonZeroU32::new(8),
11854 ..Default::default()
11855 },
11856 );
11857 });
11858
11859 editor.update_in(cx, |editor, window, cx| {
11860 editor.set_text("something_new\n", window, cx)
11861 });
11862 assert!(cx.read(|cx| editor.is_dirty(cx)));
11863 let save = editor
11864 .update_in(cx, |editor, window, cx| {
11865 editor.save(
11866 SaveOptions {
11867 format: true,
11868 autosave: false,
11869 },
11870 project.clone(),
11871 window,
11872 cx,
11873 )
11874 })
11875 .unwrap();
11876 fake_server
11877 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11878 assert_eq!(
11879 params.text_document.uri,
11880 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11881 );
11882 assert_eq!(params.options.tab_size, 8);
11883 Ok(Some(Vec::new()))
11884 })
11885 .next()
11886 .await;
11887 save.await;
11888}
11889
11890#[gpui::test]
11891async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
11892 init_test(cx, |settings| {
11893 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
11894 Formatter::LanguageServer { name: None },
11895 )))
11896 });
11897
11898 let fs = FakeFs::new(cx.executor());
11899 fs.insert_file(path!("/file.rs"), Default::default()).await;
11900
11901 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11902
11903 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11904 language_registry.add(Arc::new(Language::new(
11905 LanguageConfig {
11906 name: "Rust".into(),
11907 matcher: LanguageMatcher {
11908 path_suffixes: vec!["rs".to_string()],
11909 ..Default::default()
11910 },
11911 ..LanguageConfig::default()
11912 },
11913 Some(tree_sitter_rust::LANGUAGE.into()),
11914 )));
11915 update_test_language_settings(cx, |settings| {
11916 // Enable Prettier formatting for the same buffer, and ensure
11917 // LSP is called instead of Prettier.
11918 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
11919 });
11920 let mut fake_servers = language_registry.register_fake_lsp(
11921 "Rust",
11922 FakeLspAdapter {
11923 capabilities: lsp::ServerCapabilities {
11924 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11925 ..Default::default()
11926 },
11927 ..Default::default()
11928 },
11929 );
11930
11931 let buffer = project
11932 .update(cx, |project, cx| {
11933 project.open_local_buffer(path!("/file.rs"), cx)
11934 })
11935 .await
11936 .unwrap();
11937
11938 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11939 let (editor, cx) = cx.add_window_view(|window, cx| {
11940 build_editor_with_project(project.clone(), buffer, window, cx)
11941 });
11942 editor.update_in(cx, |editor, window, cx| {
11943 editor.set_text("one\ntwo\nthree\n", window, cx)
11944 });
11945
11946 cx.executor().start_waiting();
11947 let fake_server = fake_servers.next().await.unwrap();
11948
11949 let format = editor
11950 .update_in(cx, |editor, window, cx| {
11951 editor.perform_format(
11952 project.clone(),
11953 FormatTrigger::Manual,
11954 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11955 window,
11956 cx,
11957 )
11958 })
11959 .unwrap();
11960 fake_server
11961 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11962 assert_eq!(
11963 params.text_document.uri,
11964 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11965 );
11966 assert_eq!(params.options.tab_size, 4);
11967 Ok(Some(vec![lsp::TextEdit::new(
11968 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11969 ", ".to_string(),
11970 )]))
11971 })
11972 .next()
11973 .await;
11974 cx.executor().start_waiting();
11975 format.await;
11976 assert_eq!(
11977 editor.update(cx, |editor, cx| editor.text(cx)),
11978 "one, two\nthree\n"
11979 );
11980
11981 editor.update_in(cx, |editor, window, cx| {
11982 editor.set_text("one\ntwo\nthree\n", window, cx)
11983 });
11984 // Ensure we don't lock if formatting hangs.
11985 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11986 move |params, _| async move {
11987 assert_eq!(
11988 params.text_document.uri,
11989 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11990 );
11991 futures::future::pending::<()>().await;
11992 unreachable!()
11993 },
11994 );
11995 let format = editor
11996 .update_in(cx, |editor, window, cx| {
11997 editor.perform_format(
11998 project,
11999 FormatTrigger::Manual,
12000 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12001 window,
12002 cx,
12003 )
12004 })
12005 .unwrap();
12006 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12007 cx.executor().start_waiting();
12008 format.await;
12009 assert_eq!(
12010 editor.update(cx, |editor, cx| editor.text(cx)),
12011 "one\ntwo\nthree\n"
12012 );
12013}
12014
12015#[gpui::test]
12016async fn test_multiple_formatters(cx: &mut TestAppContext) {
12017 init_test(cx, |settings| {
12018 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12019 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
12020 Formatter::LanguageServer { name: None },
12021 Formatter::CodeAction("code-action-1".into()),
12022 Formatter::CodeAction("code-action-2".into()),
12023 ])))
12024 });
12025
12026 let fs = FakeFs::new(cx.executor());
12027 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12028 .await;
12029
12030 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12031 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12032 language_registry.add(rust_lang());
12033
12034 let mut fake_servers = language_registry.register_fake_lsp(
12035 "Rust",
12036 FakeLspAdapter {
12037 capabilities: lsp::ServerCapabilities {
12038 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12039 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12040 commands: vec!["the-command-for-code-action-1".into()],
12041 ..Default::default()
12042 }),
12043 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12044 ..Default::default()
12045 },
12046 ..Default::default()
12047 },
12048 );
12049
12050 let buffer = project
12051 .update(cx, |project, cx| {
12052 project.open_local_buffer(path!("/file.rs"), cx)
12053 })
12054 .await
12055 .unwrap();
12056
12057 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12058 let (editor, cx) = cx.add_window_view(|window, cx| {
12059 build_editor_with_project(project.clone(), buffer, window, cx)
12060 });
12061
12062 cx.executor().start_waiting();
12063
12064 let fake_server = fake_servers.next().await.unwrap();
12065 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12066 move |_params, _| async move {
12067 Ok(Some(vec![lsp::TextEdit::new(
12068 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12069 "applied-formatting\n".to_string(),
12070 )]))
12071 },
12072 );
12073 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12074 move |params, _| async move {
12075 let requested_code_actions = params.context.only.expect("Expected code action request");
12076 assert_eq!(requested_code_actions.len(), 1);
12077
12078 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12079 let code_action = match requested_code_actions[0].as_str() {
12080 "code-action-1" => lsp::CodeAction {
12081 kind: Some("code-action-1".into()),
12082 edit: Some(lsp::WorkspaceEdit::new(
12083 [(
12084 uri,
12085 vec![lsp::TextEdit::new(
12086 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12087 "applied-code-action-1-edit\n".to_string(),
12088 )],
12089 )]
12090 .into_iter()
12091 .collect(),
12092 )),
12093 command: Some(lsp::Command {
12094 command: "the-command-for-code-action-1".into(),
12095 ..Default::default()
12096 }),
12097 ..Default::default()
12098 },
12099 "code-action-2" => lsp::CodeAction {
12100 kind: Some("code-action-2".into()),
12101 edit: Some(lsp::WorkspaceEdit::new(
12102 [(
12103 uri,
12104 vec![lsp::TextEdit::new(
12105 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12106 "applied-code-action-2-edit\n".to_string(),
12107 )],
12108 )]
12109 .into_iter()
12110 .collect(),
12111 )),
12112 ..Default::default()
12113 },
12114 req => panic!("Unexpected code action request: {:?}", req),
12115 };
12116 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12117 code_action,
12118 )]))
12119 },
12120 );
12121
12122 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12123 move |params, _| async move { Ok(params) }
12124 });
12125
12126 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12127 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12128 let fake = fake_server.clone();
12129 let lock = command_lock.clone();
12130 move |params, _| {
12131 assert_eq!(params.command, "the-command-for-code-action-1");
12132 let fake = fake.clone();
12133 let lock = lock.clone();
12134 async move {
12135 lock.lock().await;
12136 fake.server
12137 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12138 label: None,
12139 edit: lsp::WorkspaceEdit {
12140 changes: Some(
12141 [(
12142 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12143 vec![lsp::TextEdit {
12144 range: lsp::Range::new(
12145 lsp::Position::new(0, 0),
12146 lsp::Position::new(0, 0),
12147 ),
12148 new_text: "applied-code-action-1-command\n".into(),
12149 }],
12150 )]
12151 .into_iter()
12152 .collect(),
12153 ),
12154 ..Default::default()
12155 },
12156 })
12157 .await
12158 .into_response()
12159 .unwrap();
12160 Ok(Some(json!(null)))
12161 }
12162 }
12163 });
12164
12165 cx.executor().start_waiting();
12166 editor
12167 .update_in(cx, |editor, window, cx| {
12168 editor.perform_format(
12169 project.clone(),
12170 FormatTrigger::Manual,
12171 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12172 window,
12173 cx,
12174 )
12175 })
12176 .unwrap()
12177 .await;
12178 editor.update(cx, |editor, cx| {
12179 assert_eq!(
12180 editor.text(cx),
12181 r#"
12182 applied-code-action-2-edit
12183 applied-code-action-1-command
12184 applied-code-action-1-edit
12185 applied-formatting
12186 one
12187 two
12188 three
12189 "#
12190 .unindent()
12191 );
12192 });
12193
12194 editor.update_in(cx, |editor, window, cx| {
12195 editor.undo(&Default::default(), window, cx);
12196 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12197 });
12198
12199 // Perform a manual edit while waiting for an LSP command
12200 // that's being run as part of a formatting code action.
12201 let lock_guard = command_lock.lock().await;
12202 let format = editor
12203 .update_in(cx, |editor, window, cx| {
12204 editor.perform_format(
12205 project.clone(),
12206 FormatTrigger::Manual,
12207 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12208 window,
12209 cx,
12210 )
12211 })
12212 .unwrap();
12213 cx.run_until_parked();
12214 editor.update(cx, |editor, cx| {
12215 assert_eq!(
12216 editor.text(cx),
12217 r#"
12218 applied-code-action-1-edit
12219 applied-formatting
12220 one
12221 two
12222 three
12223 "#
12224 .unindent()
12225 );
12226
12227 editor.buffer.update(cx, |buffer, cx| {
12228 let ix = buffer.len(cx);
12229 buffer.edit([(ix..ix, "edited\n")], None, cx);
12230 });
12231 });
12232
12233 // Allow the LSP command to proceed. Because the buffer was edited,
12234 // the second code action will not be run.
12235 drop(lock_guard);
12236 format.await;
12237 editor.update_in(cx, |editor, window, cx| {
12238 assert_eq!(
12239 editor.text(cx),
12240 r#"
12241 applied-code-action-1-command
12242 applied-code-action-1-edit
12243 applied-formatting
12244 one
12245 two
12246 three
12247 edited
12248 "#
12249 .unindent()
12250 );
12251
12252 // The manual edit is undone first, because it is the last thing the user did
12253 // (even though the command completed afterwards).
12254 editor.undo(&Default::default(), 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 "#
12265 .unindent()
12266 );
12267
12268 // All the formatting (including the command, which completed after the manual edit)
12269 // is undone together.
12270 editor.undo(&Default::default(), window, cx);
12271 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12272 });
12273}
12274
12275#[gpui::test]
12276async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12277 init_test(cx, |settings| {
12278 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
12279 Formatter::LanguageServer { name: None },
12280 ])))
12281 });
12282
12283 let fs = FakeFs::new(cx.executor());
12284 fs.insert_file(path!("/file.ts"), Default::default()).await;
12285
12286 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12287
12288 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12289 language_registry.add(Arc::new(Language::new(
12290 LanguageConfig {
12291 name: "TypeScript".into(),
12292 matcher: LanguageMatcher {
12293 path_suffixes: vec!["ts".to_string()],
12294 ..Default::default()
12295 },
12296 ..LanguageConfig::default()
12297 },
12298 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12299 )));
12300 update_test_language_settings(cx, |settings| {
12301 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12302 });
12303 let mut fake_servers = language_registry.register_fake_lsp(
12304 "TypeScript",
12305 FakeLspAdapter {
12306 capabilities: lsp::ServerCapabilities {
12307 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12308 ..Default::default()
12309 },
12310 ..Default::default()
12311 },
12312 );
12313
12314 let buffer = project
12315 .update(cx, |project, cx| {
12316 project.open_local_buffer(path!("/file.ts"), cx)
12317 })
12318 .await
12319 .unwrap();
12320
12321 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12322 let (editor, cx) = cx.add_window_view(|window, cx| {
12323 build_editor_with_project(project.clone(), buffer, window, cx)
12324 });
12325 editor.update_in(cx, |editor, window, cx| {
12326 editor.set_text(
12327 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12328 window,
12329 cx,
12330 )
12331 });
12332
12333 cx.executor().start_waiting();
12334 let fake_server = fake_servers.next().await.unwrap();
12335
12336 let format = editor
12337 .update_in(cx, |editor, window, cx| {
12338 editor.perform_code_action_kind(
12339 project.clone(),
12340 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12341 window,
12342 cx,
12343 )
12344 })
12345 .unwrap();
12346 fake_server
12347 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12348 assert_eq!(
12349 params.text_document.uri,
12350 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12351 );
12352 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12353 lsp::CodeAction {
12354 title: "Organize Imports".to_string(),
12355 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12356 edit: Some(lsp::WorkspaceEdit {
12357 changes: Some(
12358 [(
12359 params.text_document.uri.clone(),
12360 vec![lsp::TextEdit::new(
12361 lsp::Range::new(
12362 lsp::Position::new(1, 0),
12363 lsp::Position::new(2, 0),
12364 ),
12365 "".to_string(),
12366 )],
12367 )]
12368 .into_iter()
12369 .collect(),
12370 ),
12371 ..Default::default()
12372 }),
12373 ..Default::default()
12374 },
12375 )]))
12376 })
12377 .next()
12378 .await;
12379 cx.executor().start_waiting();
12380 format.await;
12381 assert_eq!(
12382 editor.update(cx, |editor, cx| editor.text(cx)),
12383 "import { a } from 'module';\n\nconst x = a;\n"
12384 );
12385
12386 editor.update_in(cx, |editor, window, cx| {
12387 editor.set_text(
12388 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12389 window,
12390 cx,
12391 )
12392 });
12393 // Ensure we don't lock if code action hangs.
12394 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12395 move |params, _| async move {
12396 assert_eq!(
12397 params.text_document.uri,
12398 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12399 );
12400 futures::future::pending::<()>().await;
12401 unreachable!()
12402 },
12403 );
12404 let format = editor
12405 .update_in(cx, |editor, window, cx| {
12406 editor.perform_code_action_kind(
12407 project,
12408 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12409 window,
12410 cx,
12411 )
12412 })
12413 .unwrap();
12414 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12415 cx.executor().start_waiting();
12416 format.await;
12417 assert_eq!(
12418 editor.update(cx, |editor, cx| editor.text(cx)),
12419 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12420 );
12421}
12422
12423#[gpui::test]
12424async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12425 init_test(cx, |_| {});
12426
12427 let mut cx = EditorLspTestContext::new_rust(
12428 lsp::ServerCapabilities {
12429 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12430 ..Default::default()
12431 },
12432 cx,
12433 )
12434 .await;
12435
12436 cx.set_state(indoc! {"
12437 one.twoˇ
12438 "});
12439
12440 // The format request takes a long time. When it completes, it inserts
12441 // a newline and an indent before the `.`
12442 cx.lsp
12443 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12444 let executor = cx.background_executor().clone();
12445 async move {
12446 executor.timer(Duration::from_millis(100)).await;
12447 Ok(Some(vec![lsp::TextEdit {
12448 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12449 new_text: "\n ".into(),
12450 }]))
12451 }
12452 });
12453
12454 // Submit a format request.
12455 let format_1 = cx
12456 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12457 .unwrap();
12458 cx.executor().run_until_parked();
12459
12460 // Submit a second format request.
12461 let format_2 = cx
12462 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12463 .unwrap();
12464 cx.executor().run_until_parked();
12465
12466 // Wait for both format requests to complete
12467 cx.executor().advance_clock(Duration::from_millis(200));
12468 cx.executor().start_waiting();
12469 format_1.await.unwrap();
12470 cx.executor().start_waiting();
12471 format_2.await.unwrap();
12472
12473 // The formatting edits only happens once.
12474 cx.assert_editor_state(indoc! {"
12475 one
12476 .twoˇ
12477 "});
12478}
12479
12480#[gpui::test]
12481async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12482 init_test(cx, |settings| {
12483 settings.defaults.formatter = Some(SelectedFormatter::Auto)
12484 });
12485
12486 let mut cx = EditorLspTestContext::new_rust(
12487 lsp::ServerCapabilities {
12488 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12489 ..Default::default()
12490 },
12491 cx,
12492 )
12493 .await;
12494
12495 // Set up a buffer white some trailing whitespace and no trailing newline.
12496 cx.set_state(
12497 &[
12498 "one ", //
12499 "twoˇ", //
12500 "three ", //
12501 "four", //
12502 ]
12503 .join("\n"),
12504 );
12505
12506 // Record which buffer changes have been sent to the language server
12507 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12508 cx.lsp
12509 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12510 let buffer_changes = buffer_changes.clone();
12511 move |params, _| {
12512 buffer_changes.lock().extend(
12513 params
12514 .content_changes
12515 .into_iter()
12516 .map(|e| (e.range.unwrap(), e.text)),
12517 );
12518 }
12519 });
12520
12521 // Handle formatting requests to the language server.
12522 cx.lsp
12523 .set_request_handler::<lsp::request::Formatting, _, _>({
12524 let buffer_changes = buffer_changes.clone();
12525 move |_, _| {
12526 let buffer_changes = buffer_changes.clone();
12527 // Insert blank lines between each line of the buffer.
12528 async move {
12529 // When formatting is requested, trailing whitespace has already been stripped,
12530 // and the trailing newline has already been added.
12531 assert_eq!(
12532 &buffer_changes.lock()[1..],
12533 &[
12534 (
12535 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12536 "".into()
12537 ),
12538 (
12539 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12540 "".into()
12541 ),
12542 (
12543 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12544 "\n".into()
12545 ),
12546 ]
12547 );
12548
12549 Ok(Some(vec![
12550 lsp::TextEdit {
12551 range: lsp::Range::new(
12552 lsp::Position::new(1, 0),
12553 lsp::Position::new(1, 0),
12554 ),
12555 new_text: "\n".into(),
12556 },
12557 lsp::TextEdit {
12558 range: lsp::Range::new(
12559 lsp::Position::new(2, 0),
12560 lsp::Position::new(2, 0),
12561 ),
12562 new_text: "\n".into(),
12563 },
12564 ]))
12565 }
12566 }
12567 });
12568
12569 // Submit a format request.
12570 let format = cx
12571 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12572 .unwrap();
12573
12574 cx.run_until_parked();
12575 // After formatting the buffer, the trailing whitespace is stripped,
12576 // a newline is appended, and the edits provided by the language server
12577 // have been applied.
12578 format.await.unwrap();
12579
12580 cx.assert_editor_state(
12581 &[
12582 "one", //
12583 "", //
12584 "twoˇ", //
12585 "", //
12586 "three", //
12587 "four", //
12588 "", //
12589 ]
12590 .join("\n"),
12591 );
12592
12593 // Undoing the formatting undoes the trailing whitespace removal, the
12594 // trailing newline, and the LSP edits.
12595 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12596 cx.assert_editor_state(
12597 &[
12598 "one ", //
12599 "twoˇ", //
12600 "three ", //
12601 "four", //
12602 ]
12603 .join("\n"),
12604 );
12605}
12606
12607#[gpui::test]
12608async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12609 cx: &mut TestAppContext,
12610) {
12611 init_test(cx, |_| {});
12612
12613 cx.update(|cx| {
12614 cx.update_global::<SettingsStore, _>(|settings, cx| {
12615 settings.update_user_settings(cx, |settings| {
12616 settings.editor.auto_signature_help = Some(true);
12617 });
12618 });
12619 });
12620
12621 let mut cx = EditorLspTestContext::new_rust(
12622 lsp::ServerCapabilities {
12623 signature_help_provider: Some(lsp::SignatureHelpOptions {
12624 ..Default::default()
12625 }),
12626 ..Default::default()
12627 },
12628 cx,
12629 )
12630 .await;
12631
12632 let language = Language::new(
12633 LanguageConfig {
12634 name: "Rust".into(),
12635 brackets: BracketPairConfig {
12636 pairs: vec![
12637 BracketPair {
12638 start: "{".to_string(),
12639 end: "}".to_string(),
12640 close: true,
12641 surround: true,
12642 newline: true,
12643 },
12644 BracketPair {
12645 start: "(".to_string(),
12646 end: ")".to_string(),
12647 close: true,
12648 surround: true,
12649 newline: true,
12650 },
12651 BracketPair {
12652 start: "/*".to_string(),
12653 end: " */".to_string(),
12654 close: true,
12655 surround: true,
12656 newline: true,
12657 },
12658 BracketPair {
12659 start: "[".to_string(),
12660 end: "]".to_string(),
12661 close: false,
12662 surround: false,
12663 newline: true,
12664 },
12665 BracketPair {
12666 start: "\"".to_string(),
12667 end: "\"".to_string(),
12668 close: true,
12669 surround: true,
12670 newline: false,
12671 },
12672 BracketPair {
12673 start: "<".to_string(),
12674 end: ">".to_string(),
12675 close: false,
12676 surround: true,
12677 newline: true,
12678 },
12679 ],
12680 ..Default::default()
12681 },
12682 autoclose_before: "})]".to_string(),
12683 ..Default::default()
12684 },
12685 Some(tree_sitter_rust::LANGUAGE.into()),
12686 );
12687 let language = Arc::new(language);
12688
12689 cx.language_registry().add(language.clone());
12690 cx.update_buffer(|buffer, cx| {
12691 buffer.set_language(Some(language), cx);
12692 });
12693
12694 cx.set_state(
12695 &r#"
12696 fn main() {
12697 sampleˇ
12698 }
12699 "#
12700 .unindent(),
12701 );
12702
12703 cx.update_editor(|editor, window, cx| {
12704 editor.handle_input("(", window, cx);
12705 });
12706 cx.assert_editor_state(
12707 &"
12708 fn main() {
12709 sample(ˇ)
12710 }
12711 "
12712 .unindent(),
12713 );
12714
12715 let mocked_response = lsp::SignatureHelp {
12716 signatures: vec![lsp::SignatureInformation {
12717 label: "fn sample(param1: u8, param2: u8)".to_string(),
12718 documentation: None,
12719 parameters: Some(vec![
12720 lsp::ParameterInformation {
12721 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12722 documentation: None,
12723 },
12724 lsp::ParameterInformation {
12725 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12726 documentation: None,
12727 },
12728 ]),
12729 active_parameter: None,
12730 }],
12731 active_signature: Some(0),
12732 active_parameter: Some(0),
12733 };
12734 handle_signature_help_request(&mut cx, mocked_response).await;
12735
12736 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12737 .await;
12738
12739 cx.editor(|editor, _, _| {
12740 let signature_help_state = editor.signature_help_state.popover().cloned();
12741 let signature = signature_help_state.unwrap();
12742 assert_eq!(
12743 signature.signatures[signature.current_signature].label,
12744 "fn sample(param1: u8, param2: u8)"
12745 );
12746 });
12747}
12748
12749#[gpui::test]
12750async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12751 init_test(cx, |_| {});
12752
12753 cx.update(|cx| {
12754 cx.update_global::<SettingsStore, _>(|settings, cx| {
12755 settings.update_user_settings(cx, |settings| {
12756 settings.editor.auto_signature_help = Some(false);
12757 settings.editor.show_signature_help_after_edits = Some(false);
12758 });
12759 });
12760 });
12761
12762 let mut cx = EditorLspTestContext::new_rust(
12763 lsp::ServerCapabilities {
12764 signature_help_provider: Some(lsp::SignatureHelpOptions {
12765 ..Default::default()
12766 }),
12767 ..Default::default()
12768 },
12769 cx,
12770 )
12771 .await;
12772
12773 let language = Language::new(
12774 LanguageConfig {
12775 name: "Rust".into(),
12776 brackets: BracketPairConfig {
12777 pairs: vec![
12778 BracketPair {
12779 start: "{".to_string(),
12780 end: "}".to_string(),
12781 close: true,
12782 surround: true,
12783 newline: true,
12784 },
12785 BracketPair {
12786 start: "(".to_string(),
12787 end: ")".to_string(),
12788 close: true,
12789 surround: true,
12790 newline: true,
12791 },
12792 BracketPair {
12793 start: "/*".to_string(),
12794 end: " */".to_string(),
12795 close: true,
12796 surround: true,
12797 newline: true,
12798 },
12799 BracketPair {
12800 start: "[".to_string(),
12801 end: "]".to_string(),
12802 close: false,
12803 surround: false,
12804 newline: true,
12805 },
12806 BracketPair {
12807 start: "\"".to_string(),
12808 end: "\"".to_string(),
12809 close: true,
12810 surround: true,
12811 newline: false,
12812 },
12813 BracketPair {
12814 start: "<".to_string(),
12815 end: ">".to_string(),
12816 close: false,
12817 surround: true,
12818 newline: true,
12819 },
12820 ],
12821 ..Default::default()
12822 },
12823 autoclose_before: "})]".to_string(),
12824 ..Default::default()
12825 },
12826 Some(tree_sitter_rust::LANGUAGE.into()),
12827 );
12828 let language = Arc::new(language);
12829
12830 cx.language_registry().add(language.clone());
12831 cx.update_buffer(|buffer, cx| {
12832 buffer.set_language(Some(language), cx);
12833 });
12834
12835 // Ensure that signature_help is not called when no signature help is enabled.
12836 cx.set_state(
12837 &r#"
12838 fn main() {
12839 sampleˇ
12840 }
12841 "#
12842 .unindent(),
12843 );
12844 cx.update_editor(|editor, window, cx| {
12845 editor.handle_input("(", window, cx);
12846 });
12847 cx.assert_editor_state(
12848 &"
12849 fn main() {
12850 sample(ˇ)
12851 }
12852 "
12853 .unindent(),
12854 );
12855 cx.editor(|editor, _, _| {
12856 assert!(editor.signature_help_state.task().is_none());
12857 });
12858
12859 let mocked_response = lsp::SignatureHelp {
12860 signatures: vec![lsp::SignatureInformation {
12861 label: "fn sample(param1: u8, param2: u8)".to_string(),
12862 documentation: None,
12863 parameters: Some(vec![
12864 lsp::ParameterInformation {
12865 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12866 documentation: None,
12867 },
12868 lsp::ParameterInformation {
12869 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12870 documentation: None,
12871 },
12872 ]),
12873 active_parameter: None,
12874 }],
12875 active_signature: Some(0),
12876 active_parameter: Some(0),
12877 };
12878
12879 // Ensure that signature_help is called when enabled afte edits
12880 cx.update(|_, cx| {
12881 cx.update_global::<SettingsStore, _>(|settings, cx| {
12882 settings.update_user_settings(cx, |settings| {
12883 settings.editor.auto_signature_help = Some(false);
12884 settings.editor.show_signature_help_after_edits = Some(true);
12885 });
12886 });
12887 });
12888 cx.set_state(
12889 &r#"
12890 fn main() {
12891 sampleˇ
12892 }
12893 "#
12894 .unindent(),
12895 );
12896 cx.update_editor(|editor, window, cx| {
12897 editor.handle_input("(", window, cx);
12898 });
12899 cx.assert_editor_state(
12900 &"
12901 fn main() {
12902 sample(ˇ)
12903 }
12904 "
12905 .unindent(),
12906 );
12907 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12908 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12909 .await;
12910 cx.update_editor(|editor, _, _| {
12911 let signature_help_state = editor.signature_help_state.popover().cloned();
12912 assert!(signature_help_state.is_some());
12913 let signature = signature_help_state.unwrap();
12914 assert_eq!(
12915 signature.signatures[signature.current_signature].label,
12916 "fn sample(param1: u8, param2: u8)"
12917 );
12918 editor.signature_help_state = SignatureHelpState::default();
12919 });
12920
12921 // Ensure that signature_help is called when auto signature help override is enabled
12922 cx.update(|_, cx| {
12923 cx.update_global::<SettingsStore, _>(|settings, cx| {
12924 settings.update_user_settings(cx, |settings| {
12925 settings.editor.auto_signature_help = Some(true);
12926 settings.editor.show_signature_help_after_edits = Some(false);
12927 });
12928 });
12929 });
12930 cx.set_state(
12931 &r#"
12932 fn main() {
12933 sampleˇ
12934 }
12935 "#
12936 .unindent(),
12937 );
12938 cx.update_editor(|editor, window, cx| {
12939 editor.handle_input("(", window, cx);
12940 });
12941 cx.assert_editor_state(
12942 &"
12943 fn main() {
12944 sample(ˇ)
12945 }
12946 "
12947 .unindent(),
12948 );
12949 handle_signature_help_request(&mut cx, mocked_response).await;
12950 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12951 .await;
12952 cx.editor(|editor, _, _| {
12953 let signature_help_state = editor.signature_help_state.popover().cloned();
12954 assert!(signature_help_state.is_some());
12955 let signature = signature_help_state.unwrap();
12956 assert_eq!(
12957 signature.signatures[signature.current_signature].label,
12958 "fn sample(param1: u8, param2: u8)"
12959 );
12960 });
12961}
12962
12963#[gpui::test]
12964async fn test_signature_help(cx: &mut TestAppContext) {
12965 init_test(cx, |_| {});
12966 cx.update(|cx| {
12967 cx.update_global::<SettingsStore, _>(|settings, cx| {
12968 settings.update_user_settings(cx, |settings| {
12969 settings.editor.auto_signature_help = Some(true);
12970 });
12971 });
12972 });
12973
12974 let mut cx = EditorLspTestContext::new_rust(
12975 lsp::ServerCapabilities {
12976 signature_help_provider: Some(lsp::SignatureHelpOptions {
12977 ..Default::default()
12978 }),
12979 ..Default::default()
12980 },
12981 cx,
12982 )
12983 .await;
12984
12985 // A test that directly calls `show_signature_help`
12986 cx.update_editor(|editor, window, cx| {
12987 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12988 });
12989
12990 let mocked_response = lsp::SignatureHelp {
12991 signatures: vec![lsp::SignatureInformation {
12992 label: "fn sample(param1: u8, param2: u8)".to_string(),
12993 documentation: None,
12994 parameters: Some(vec![
12995 lsp::ParameterInformation {
12996 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12997 documentation: None,
12998 },
12999 lsp::ParameterInformation {
13000 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13001 documentation: None,
13002 },
13003 ]),
13004 active_parameter: None,
13005 }],
13006 active_signature: Some(0),
13007 active_parameter: Some(0),
13008 };
13009 handle_signature_help_request(&mut cx, mocked_response).await;
13010
13011 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13012 .await;
13013
13014 cx.editor(|editor, _, _| {
13015 let signature_help_state = editor.signature_help_state.popover().cloned();
13016 assert!(signature_help_state.is_some());
13017 let signature = signature_help_state.unwrap();
13018 assert_eq!(
13019 signature.signatures[signature.current_signature].label,
13020 "fn sample(param1: u8, param2: u8)"
13021 );
13022 });
13023
13024 // When exiting outside from inside the brackets, `signature_help` is closed.
13025 cx.set_state(indoc! {"
13026 fn main() {
13027 sample(ˇ);
13028 }
13029
13030 fn sample(param1: u8, param2: u8) {}
13031 "});
13032
13033 cx.update_editor(|editor, window, cx| {
13034 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13035 s.select_ranges([0..0])
13036 });
13037 });
13038
13039 let mocked_response = lsp::SignatureHelp {
13040 signatures: Vec::new(),
13041 active_signature: None,
13042 active_parameter: None,
13043 };
13044 handle_signature_help_request(&mut cx, mocked_response).await;
13045
13046 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13047 .await;
13048
13049 cx.editor(|editor, _, _| {
13050 assert!(!editor.signature_help_state.is_shown());
13051 });
13052
13053 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13054 cx.set_state(indoc! {"
13055 fn main() {
13056 sample(ˇ);
13057 }
13058
13059 fn sample(param1: u8, param2: u8) {}
13060 "});
13061
13062 let mocked_response = lsp::SignatureHelp {
13063 signatures: vec![lsp::SignatureInformation {
13064 label: "fn sample(param1: u8, param2: u8)".to_string(),
13065 documentation: None,
13066 parameters: Some(vec![
13067 lsp::ParameterInformation {
13068 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13069 documentation: None,
13070 },
13071 lsp::ParameterInformation {
13072 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13073 documentation: None,
13074 },
13075 ]),
13076 active_parameter: None,
13077 }],
13078 active_signature: Some(0),
13079 active_parameter: Some(0),
13080 };
13081 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13082 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13083 .await;
13084 cx.editor(|editor, _, _| {
13085 assert!(editor.signature_help_state.is_shown());
13086 });
13087
13088 // Restore the popover with more parameter input
13089 cx.set_state(indoc! {"
13090 fn main() {
13091 sample(param1, param2ˇ);
13092 }
13093
13094 fn sample(param1: u8, param2: u8) {}
13095 "});
13096
13097 let mocked_response = lsp::SignatureHelp {
13098 signatures: vec![lsp::SignatureInformation {
13099 label: "fn sample(param1: u8, param2: u8)".to_string(),
13100 documentation: None,
13101 parameters: Some(vec![
13102 lsp::ParameterInformation {
13103 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13104 documentation: None,
13105 },
13106 lsp::ParameterInformation {
13107 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13108 documentation: None,
13109 },
13110 ]),
13111 active_parameter: None,
13112 }],
13113 active_signature: Some(0),
13114 active_parameter: Some(1),
13115 };
13116 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13117 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13118 .await;
13119
13120 // When selecting a range, the popover is gone.
13121 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13122 cx.update_editor(|editor, window, cx| {
13123 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13124 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13125 })
13126 });
13127 cx.assert_editor_state(indoc! {"
13128 fn main() {
13129 sample(param1, «ˇparam2»);
13130 }
13131
13132 fn sample(param1: u8, param2: u8) {}
13133 "});
13134 cx.editor(|editor, _, _| {
13135 assert!(!editor.signature_help_state.is_shown());
13136 });
13137
13138 // When unselecting again, the popover is back if within the brackets.
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, 19)..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 handle_signature_help_request(&mut cx, mocked_response).await;
13152 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13153 .await;
13154 cx.editor(|editor, _, _| {
13155 assert!(editor.signature_help_state.is_shown());
13156 });
13157
13158 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13159 cx.update_editor(|editor, window, cx| {
13160 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13161 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13162 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13163 })
13164 });
13165 cx.assert_editor_state(indoc! {"
13166 fn main() {
13167 sample(param1, ˇparam2);
13168 }
13169
13170 fn sample(param1: u8, param2: u8) {}
13171 "});
13172
13173 let mocked_response = lsp::SignatureHelp {
13174 signatures: vec![lsp::SignatureInformation {
13175 label: "fn sample(param1: u8, param2: u8)".to_string(),
13176 documentation: None,
13177 parameters: Some(vec![
13178 lsp::ParameterInformation {
13179 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13180 documentation: None,
13181 },
13182 lsp::ParameterInformation {
13183 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13184 documentation: None,
13185 },
13186 ]),
13187 active_parameter: None,
13188 }],
13189 active_signature: Some(0),
13190 active_parameter: Some(1),
13191 };
13192 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13193 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13194 .await;
13195 cx.update_editor(|editor, _, cx| {
13196 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13197 });
13198 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13199 .await;
13200 cx.update_editor(|editor, window, cx| {
13201 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13202 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13203 })
13204 });
13205 cx.assert_editor_state(indoc! {"
13206 fn main() {
13207 sample(param1, «ˇparam2»);
13208 }
13209
13210 fn sample(param1: u8, param2: u8) {}
13211 "});
13212 cx.update_editor(|editor, window, cx| {
13213 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13214 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13215 })
13216 });
13217 cx.assert_editor_state(indoc! {"
13218 fn main() {
13219 sample(param1, ˇparam2);
13220 }
13221
13222 fn sample(param1: u8, param2: u8) {}
13223 "});
13224 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13225 .await;
13226}
13227
13228#[gpui::test]
13229async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13230 init_test(cx, |_| {});
13231
13232 let mut cx = EditorLspTestContext::new_rust(
13233 lsp::ServerCapabilities {
13234 signature_help_provider: Some(lsp::SignatureHelpOptions {
13235 ..Default::default()
13236 }),
13237 ..Default::default()
13238 },
13239 cx,
13240 )
13241 .await;
13242
13243 cx.set_state(indoc! {"
13244 fn main() {
13245 overloadedˇ
13246 }
13247 "});
13248
13249 cx.update_editor(|editor, window, cx| {
13250 editor.handle_input("(", window, cx);
13251 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13252 });
13253
13254 // Mock response with 3 signatures
13255 let mocked_response = lsp::SignatureHelp {
13256 signatures: vec![
13257 lsp::SignatureInformation {
13258 label: "fn overloaded(x: i32)".to_string(),
13259 documentation: None,
13260 parameters: Some(vec![lsp::ParameterInformation {
13261 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13262 documentation: None,
13263 }]),
13264 active_parameter: None,
13265 },
13266 lsp::SignatureInformation {
13267 label: "fn overloaded(x: i32, y: i32)".to_string(),
13268 documentation: None,
13269 parameters: Some(vec![
13270 lsp::ParameterInformation {
13271 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13272 documentation: None,
13273 },
13274 lsp::ParameterInformation {
13275 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13276 documentation: None,
13277 },
13278 ]),
13279 active_parameter: None,
13280 },
13281 lsp::SignatureInformation {
13282 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13283 documentation: None,
13284 parameters: Some(vec![
13285 lsp::ParameterInformation {
13286 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13287 documentation: None,
13288 },
13289 lsp::ParameterInformation {
13290 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13291 documentation: None,
13292 },
13293 lsp::ParameterInformation {
13294 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13295 documentation: None,
13296 },
13297 ]),
13298 active_parameter: None,
13299 },
13300 ],
13301 active_signature: Some(1),
13302 active_parameter: Some(0),
13303 };
13304 handle_signature_help_request(&mut cx, mocked_response).await;
13305
13306 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13307 .await;
13308
13309 // Verify we have multiple signatures and the right one is selected
13310 cx.editor(|editor, _, _| {
13311 let popover = editor.signature_help_state.popover().cloned().unwrap();
13312 assert_eq!(popover.signatures.len(), 3);
13313 // active_signature was 1, so that should be the current
13314 assert_eq!(popover.current_signature, 1);
13315 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13316 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13317 assert_eq!(
13318 popover.signatures[2].label,
13319 "fn overloaded(x: i32, y: i32, z: i32)"
13320 );
13321 });
13322
13323 // Test navigation functionality
13324 cx.update_editor(|editor, window, cx| {
13325 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13326 });
13327
13328 cx.editor(|editor, _, _| {
13329 let popover = editor.signature_help_state.popover().cloned().unwrap();
13330 assert_eq!(popover.current_signature, 2);
13331 });
13332
13333 // Test wrap around
13334 cx.update_editor(|editor, window, cx| {
13335 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13336 });
13337
13338 cx.editor(|editor, _, _| {
13339 let popover = editor.signature_help_state.popover().cloned().unwrap();
13340 assert_eq!(popover.current_signature, 0);
13341 });
13342
13343 // Test previous navigation
13344 cx.update_editor(|editor, window, cx| {
13345 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13346 });
13347
13348 cx.editor(|editor, _, _| {
13349 let popover = editor.signature_help_state.popover().cloned().unwrap();
13350 assert_eq!(popover.current_signature, 2);
13351 });
13352}
13353
13354#[gpui::test]
13355async fn test_completion_mode(cx: &mut TestAppContext) {
13356 init_test(cx, |_| {});
13357 let mut cx = EditorLspTestContext::new_rust(
13358 lsp::ServerCapabilities {
13359 completion_provider: Some(lsp::CompletionOptions {
13360 resolve_provider: Some(true),
13361 ..Default::default()
13362 }),
13363 ..Default::default()
13364 },
13365 cx,
13366 )
13367 .await;
13368
13369 struct Run {
13370 run_description: &'static str,
13371 initial_state: String,
13372 buffer_marked_text: String,
13373 completion_label: &'static str,
13374 completion_text: &'static str,
13375 expected_with_insert_mode: String,
13376 expected_with_replace_mode: String,
13377 expected_with_replace_subsequence_mode: String,
13378 expected_with_replace_suffix_mode: String,
13379 }
13380
13381 let runs = [
13382 Run {
13383 run_description: "Start of word matches completion text",
13384 initial_state: "before ediˇ after".into(),
13385 buffer_marked_text: "before <edi|> after".into(),
13386 completion_label: "editor",
13387 completion_text: "editor",
13388 expected_with_insert_mode: "before editorˇ after".into(),
13389 expected_with_replace_mode: "before editorˇ after".into(),
13390 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13391 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13392 },
13393 Run {
13394 run_description: "Accept same text at the middle of the word",
13395 initial_state: "before ediˇtor after".into(),
13396 buffer_marked_text: "before <edi|tor> after".into(),
13397 completion_label: "editor",
13398 completion_text: "editor",
13399 expected_with_insert_mode: "before editorˇtor after".into(),
13400 expected_with_replace_mode: "before editorˇ after".into(),
13401 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13402 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13403 },
13404 Run {
13405 run_description: "End of word matches completion text -- cursor at end",
13406 initial_state: "before torˇ after".into(),
13407 buffer_marked_text: "before <tor|> after".into(),
13408 completion_label: "editor",
13409 completion_text: "editor",
13410 expected_with_insert_mode: "before editorˇ after".into(),
13411 expected_with_replace_mode: "before editorˇ after".into(),
13412 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13413 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13414 },
13415 Run {
13416 run_description: "End of word matches completion text -- cursor at start",
13417 initial_state: "before ˇtor after".into(),
13418 buffer_marked_text: "before <|tor> after".into(),
13419 completion_label: "editor",
13420 completion_text: "editor",
13421 expected_with_insert_mode: "before editorˇtor after".into(),
13422 expected_with_replace_mode: "before editorˇ after".into(),
13423 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13424 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13425 },
13426 Run {
13427 run_description: "Prepend text containing whitespace",
13428 initial_state: "pˇfield: bool".into(),
13429 buffer_marked_text: "<p|field>: bool".into(),
13430 completion_label: "pub ",
13431 completion_text: "pub ",
13432 expected_with_insert_mode: "pub ˇfield: bool".into(),
13433 expected_with_replace_mode: "pub ˇ: bool".into(),
13434 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13435 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13436 },
13437 Run {
13438 run_description: "Add element to start of list",
13439 initial_state: "[element_ˇelement_2]".into(),
13440 buffer_marked_text: "[<element_|element_2>]".into(),
13441 completion_label: "element_1",
13442 completion_text: "element_1",
13443 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13444 expected_with_replace_mode: "[element_1ˇ]".into(),
13445 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13446 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13447 },
13448 Run {
13449 run_description: "Add element to start of list -- first and second elements are equal",
13450 initial_state: "[elˇelement]".into(),
13451 buffer_marked_text: "[<el|element>]".into(),
13452 completion_label: "element",
13453 completion_text: "element",
13454 expected_with_insert_mode: "[elementˇelement]".into(),
13455 expected_with_replace_mode: "[elementˇ]".into(),
13456 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13457 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13458 },
13459 Run {
13460 run_description: "Ends with matching suffix",
13461 initial_state: "SubˇError".into(),
13462 buffer_marked_text: "<Sub|Error>".into(),
13463 completion_label: "SubscriptionError",
13464 completion_text: "SubscriptionError",
13465 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13466 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13467 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13468 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13469 },
13470 Run {
13471 run_description: "Suffix is a subsequence -- contiguous",
13472 initial_state: "SubˇErr".into(),
13473 buffer_marked_text: "<Sub|Err>".into(),
13474 completion_label: "SubscriptionError",
13475 completion_text: "SubscriptionError",
13476 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13477 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13478 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13479 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13480 },
13481 Run {
13482 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13483 initial_state: "Suˇscrirr".into(),
13484 buffer_marked_text: "<Su|scrirr>".into(),
13485 completion_label: "SubscriptionError",
13486 completion_text: "SubscriptionError",
13487 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13488 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13489 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13490 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13491 },
13492 Run {
13493 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13494 initial_state: "foo(indˇix)".into(),
13495 buffer_marked_text: "foo(<ind|ix>)".into(),
13496 completion_label: "node_index",
13497 completion_text: "node_index",
13498 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13499 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13500 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13501 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13502 },
13503 Run {
13504 run_description: "Replace range ends before cursor - should extend to cursor",
13505 initial_state: "before editˇo after".into(),
13506 buffer_marked_text: "before <{ed}>it|o after".into(),
13507 completion_label: "editor",
13508 completion_text: "editor",
13509 expected_with_insert_mode: "before editorˇo after".into(),
13510 expected_with_replace_mode: "before editorˇo after".into(),
13511 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13512 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13513 },
13514 Run {
13515 run_description: "Uses label for suffix matching",
13516 initial_state: "before ediˇtor after".into(),
13517 buffer_marked_text: "before <edi|tor> after".into(),
13518 completion_label: "editor",
13519 completion_text: "editor()",
13520 expected_with_insert_mode: "before editor()ˇtor after".into(),
13521 expected_with_replace_mode: "before editor()ˇ after".into(),
13522 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13523 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13524 },
13525 Run {
13526 run_description: "Case insensitive subsequence and suffix matching",
13527 initial_state: "before EDiˇtoR after".into(),
13528 buffer_marked_text: "before <EDi|toR> after".into(),
13529 completion_label: "editor",
13530 completion_text: "editor",
13531 expected_with_insert_mode: "before editorˇtoR after".into(),
13532 expected_with_replace_mode: "before editorˇ after".into(),
13533 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13534 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13535 },
13536 ];
13537
13538 for run in runs {
13539 let run_variations = [
13540 (LspInsertMode::Insert, run.expected_with_insert_mode),
13541 (LspInsertMode::Replace, run.expected_with_replace_mode),
13542 (
13543 LspInsertMode::ReplaceSubsequence,
13544 run.expected_with_replace_subsequence_mode,
13545 ),
13546 (
13547 LspInsertMode::ReplaceSuffix,
13548 run.expected_with_replace_suffix_mode,
13549 ),
13550 ];
13551
13552 for (lsp_insert_mode, expected_text) in run_variations {
13553 eprintln!(
13554 "run = {:?}, mode = {lsp_insert_mode:.?}",
13555 run.run_description,
13556 );
13557
13558 update_test_language_settings(&mut cx, |settings| {
13559 settings.defaults.completions = Some(CompletionSettingsContent {
13560 lsp_insert_mode: Some(lsp_insert_mode),
13561 words: Some(WordsCompletionMode::Disabled),
13562 words_min_length: Some(0),
13563 ..Default::default()
13564 });
13565 });
13566
13567 cx.set_state(&run.initial_state);
13568 cx.update_editor(|editor, window, cx| {
13569 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13570 });
13571
13572 let counter = Arc::new(AtomicUsize::new(0));
13573 handle_completion_request_with_insert_and_replace(
13574 &mut cx,
13575 &run.buffer_marked_text,
13576 vec![(run.completion_label, run.completion_text)],
13577 counter.clone(),
13578 )
13579 .await;
13580 cx.condition(|editor, _| editor.context_menu_visible())
13581 .await;
13582 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13583
13584 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13585 editor
13586 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13587 .unwrap()
13588 });
13589 cx.assert_editor_state(&expected_text);
13590 handle_resolve_completion_request(&mut cx, None).await;
13591 apply_additional_edits.await.unwrap();
13592 }
13593 }
13594}
13595
13596#[gpui::test]
13597async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13598 init_test(cx, |_| {});
13599 let mut cx = EditorLspTestContext::new_rust(
13600 lsp::ServerCapabilities {
13601 completion_provider: Some(lsp::CompletionOptions {
13602 resolve_provider: Some(true),
13603 ..Default::default()
13604 }),
13605 ..Default::default()
13606 },
13607 cx,
13608 )
13609 .await;
13610
13611 let initial_state = "SubˇError";
13612 let buffer_marked_text = "<Sub|Error>";
13613 let completion_text = "SubscriptionError";
13614 let expected_with_insert_mode = "SubscriptionErrorˇError";
13615 let expected_with_replace_mode = "SubscriptionErrorˇ";
13616
13617 update_test_language_settings(&mut cx, |settings| {
13618 settings.defaults.completions = Some(CompletionSettingsContent {
13619 words: Some(WordsCompletionMode::Disabled),
13620 words_min_length: Some(0),
13621 // set the opposite here to ensure that the action is overriding the default behavior
13622 lsp_insert_mode: Some(LspInsertMode::Insert),
13623 ..Default::default()
13624 });
13625 });
13626
13627 cx.set_state(initial_state);
13628 cx.update_editor(|editor, window, cx| {
13629 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13630 });
13631
13632 let counter = Arc::new(AtomicUsize::new(0));
13633 handle_completion_request_with_insert_and_replace(
13634 &mut cx,
13635 buffer_marked_text,
13636 vec![(completion_text, completion_text)],
13637 counter.clone(),
13638 )
13639 .await;
13640 cx.condition(|editor, _| editor.context_menu_visible())
13641 .await;
13642 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13643
13644 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13645 editor
13646 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13647 .unwrap()
13648 });
13649 cx.assert_editor_state(expected_with_replace_mode);
13650 handle_resolve_completion_request(&mut cx, None).await;
13651 apply_additional_edits.await.unwrap();
13652
13653 update_test_language_settings(&mut cx, |settings| {
13654 settings.defaults.completions = Some(CompletionSettingsContent {
13655 words: Some(WordsCompletionMode::Disabled),
13656 words_min_length: Some(0),
13657 // set the opposite here to ensure that the action is overriding the default behavior
13658 lsp_insert_mode: Some(LspInsertMode::Replace),
13659 ..Default::default()
13660 });
13661 });
13662
13663 cx.set_state(initial_state);
13664 cx.update_editor(|editor, window, cx| {
13665 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13666 });
13667 handle_completion_request_with_insert_and_replace(
13668 &mut cx,
13669 buffer_marked_text,
13670 vec![(completion_text, completion_text)],
13671 counter.clone(),
13672 )
13673 .await;
13674 cx.condition(|editor, _| editor.context_menu_visible())
13675 .await;
13676 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13677
13678 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13679 editor
13680 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13681 .unwrap()
13682 });
13683 cx.assert_editor_state(expected_with_insert_mode);
13684 handle_resolve_completion_request(&mut cx, None).await;
13685 apply_additional_edits.await.unwrap();
13686}
13687
13688#[gpui::test]
13689async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13690 init_test(cx, |_| {});
13691 let mut cx = EditorLspTestContext::new_rust(
13692 lsp::ServerCapabilities {
13693 completion_provider: Some(lsp::CompletionOptions {
13694 resolve_provider: Some(true),
13695 ..Default::default()
13696 }),
13697 ..Default::default()
13698 },
13699 cx,
13700 )
13701 .await;
13702
13703 // scenario: surrounding text matches completion text
13704 let completion_text = "to_offset";
13705 let initial_state = indoc! {"
13706 1. buf.to_offˇsuffix
13707 2. buf.to_offˇsuf
13708 3. buf.to_offˇfix
13709 4. buf.to_offˇ
13710 5. into_offˇensive
13711 6. ˇsuffix
13712 7. let ˇ //
13713 8. aaˇzz
13714 9. buf.to_off«zzzzzˇ»suffix
13715 10. buf.«ˇzzzzz»suffix
13716 11. to_off«ˇzzzzz»
13717
13718 buf.to_offˇsuffix // newest cursor
13719 "};
13720 let completion_marked_buffer = indoc! {"
13721 1. buf.to_offsuffix
13722 2. buf.to_offsuf
13723 3. buf.to_offfix
13724 4. buf.to_off
13725 5. into_offensive
13726 6. suffix
13727 7. let //
13728 8. aazz
13729 9. buf.to_offzzzzzsuffix
13730 10. buf.zzzzzsuffix
13731 11. to_offzzzzz
13732
13733 buf.<to_off|suffix> // newest cursor
13734 "};
13735 let expected = indoc! {"
13736 1. buf.to_offsetˇ
13737 2. buf.to_offsetˇsuf
13738 3. buf.to_offsetˇfix
13739 4. buf.to_offsetˇ
13740 5. into_offsetˇensive
13741 6. to_offsetˇsuffix
13742 7. let to_offsetˇ //
13743 8. aato_offsetˇzz
13744 9. buf.to_offsetˇ
13745 10. buf.to_offsetˇsuffix
13746 11. to_offsetˇ
13747
13748 buf.to_offsetˇ // newest cursor
13749 "};
13750 cx.set_state(initial_state);
13751 cx.update_editor(|editor, window, cx| {
13752 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13753 });
13754 handle_completion_request_with_insert_and_replace(
13755 &mut cx,
13756 completion_marked_buffer,
13757 vec![(completion_text, completion_text)],
13758 Arc::new(AtomicUsize::new(0)),
13759 )
13760 .await;
13761 cx.condition(|editor, _| editor.context_menu_visible())
13762 .await;
13763 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13764 editor
13765 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13766 .unwrap()
13767 });
13768 cx.assert_editor_state(expected);
13769 handle_resolve_completion_request(&mut cx, None).await;
13770 apply_additional_edits.await.unwrap();
13771
13772 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13773 let completion_text = "foo_and_bar";
13774 let initial_state = indoc! {"
13775 1. ooanbˇ
13776 2. zooanbˇ
13777 3. ooanbˇz
13778 4. zooanbˇz
13779 5. ooanˇ
13780 6. oanbˇ
13781
13782 ooanbˇ
13783 "};
13784 let completion_marked_buffer = indoc! {"
13785 1. ooanb
13786 2. zooanb
13787 3. ooanbz
13788 4. zooanbz
13789 5. ooan
13790 6. oanb
13791
13792 <ooanb|>
13793 "};
13794 let expected = indoc! {"
13795 1. foo_and_barˇ
13796 2. zfoo_and_barˇ
13797 3. foo_and_barˇz
13798 4. zfoo_and_barˇz
13799 5. ooanfoo_and_barˇ
13800 6. oanbfoo_and_barˇ
13801
13802 foo_and_barˇ
13803 "};
13804 cx.set_state(initial_state);
13805 cx.update_editor(|editor, window, cx| {
13806 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13807 });
13808 handle_completion_request_with_insert_and_replace(
13809 &mut cx,
13810 completion_marked_buffer,
13811 vec![(completion_text, completion_text)],
13812 Arc::new(AtomicUsize::new(0)),
13813 )
13814 .await;
13815 cx.condition(|editor, _| editor.context_menu_visible())
13816 .await;
13817 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13818 editor
13819 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13820 .unwrap()
13821 });
13822 cx.assert_editor_state(expected);
13823 handle_resolve_completion_request(&mut cx, None).await;
13824 apply_additional_edits.await.unwrap();
13825
13826 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13827 // (expects the same as if it was inserted at the end)
13828 let completion_text = "foo_and_bar";
13829 let initial_state = indoc! {"
13830 1. ooˇanb
13831 2. zooˇanb
13832 3. ooˇanbz
13833 4. zooˇanbz
13834
13835 ooˇanb
13836 "};
13837 let completion_marked_buffer = indoc! {"
13838 1. ooanb
13839 2. zooanb
13840 3. ooanbz
13841 4. zooanbz
13842
13843 <oo|anb>
13844 "};
13845 let expected = indoc! {"
13846 1. foo_and_barˇ
13847 2. zfoo_and_barˇ
13848 3. foo_and_barˇz
13849 4. zfoo_and_barˇz
13850
13851 foo_and_barˇ
13852 "};
13853 cx.set_state(initial_state);
13854 cx.update_editor(|editor, window, cx| {
13855 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13856 });
13857 handle_completion_request_with_insert_and_replace(
13858 &mut cx,
13859 completion_marked_buffer,
13860 vec![(completion_text, completion_text)],
13861 Arc::new(AtomicUsize::new(0)),
13862 )
13863 .await;
13864 cx.condition(|editor, _| editor.context_menu_visible())
13865 .await;
13866 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13867 editor
13868 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13869 .unwrap()
13870 });
13871 cx.assert_editor_state(expected);
13872 handle_resolve_completion_request(&mut cx, None).await;
13873 apply_additional_edits.await.unwrap();
13874}
13875
13876// This used to crash
13877#[gpui::test]
13878async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13879 init_test(cx, |_| {});
13880
13881 let buffer_text = indoc! {"
13882 fn main() {
13883 10.satu;
13884
13885 //
13886 // separate cursors so they open in different excerpts (manually reproducible)
13887 //
13888
13889 10.satu20;
13890 }
13891 "};
13892 let multibuffer_text_with_selections = indoc! {"
13893 fn main() {
13894 10.satuˇ;
13895
13896 //
13897
13898 //
13899
13900 10.satuˇ20;
13901 }
13902 "};
13903 let expected_multibuffer = indoc! {"
13904 fn main() {
13905 10.saturating_sub()ˇ;
13906
13907 //
13908
13909 //
13910
13911 10.saturating_sub()ˇ;
13912 }
13913 "};
13914
13915 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13916 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13917
13918 let fs = FakeFs::new(cx.executor());
13919 fs.insert_tree(
13920 path!("/a"),
13921 json!({
13922 "main.rs": buffer_text,
13923 }),
13924 )
13925 .await;
13926
13927 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13928 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13929 language_registry.add(rust_lang());
13930 let mut fake_servers = language_registry.register_fake_lsp(
13931 "Rust",
13932 FakeLspAdapter {
13933 capabilities: lsp::ServerCapabilities {
13934 completion_provider: Some(lsp::CompletionOptions {
13935 resolve_provider: None,
13936 ..lsp::CompletionOptions::default()
13937 }),
13938 ..lsp::ServerCapabilities::default()
13939 },
13940 ..FakeLspAdapter::default()
13941 },
13942 );
13943 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13944 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13945 let buffer = project
13946 .update(cx, |project, cx| {
13947 project.open_local_buffer(path!("/a/main.rs"), cx)
13948 })
13949 .await
13950 .unwrap();
13951
13952 let multi_buffer = cx.new(|cx| {
13953 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13954 multi_buffer.push_excerpts(
13955 buffer.clone(),
13956 [ExcerptRange::new(0..first_excerpt_end)],
13957 cx,
13958 );
13959 multi_buffer.push_excerpts(
13960 buffer.clone(),
13961 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13962 cx,
13963 );
13964 multi_buffer
13965 });
13966
13967 let editor = workspace
13968 .update(cx, |_, window, cx| {
13969 cx.new(|cx| {
13970 Editor::new(
13971 EditorMode::Full {
13972 scale_ui_elements_with_buffer_font_size: false,
13973 show_active_line_background: false,
13974 sized_by_content: false,
13975 },
13976 multi_buffer.clone(),
13977 Some(project.clone()),
13978 window,
13979 cx,
13980 )
13981 })
13982 })
13983 .unwrap();
13984
13985 let pane = workspace
13986 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13987 .unwrap();
13988 pane.update_in(cx, |pane, window, cx| {
13989 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
13990 });
13991
13992 let fake_server = fake_servers.next().await.unwrap();
13993
13994 editor.update_in(cx, |editor, window, cx| {
13995 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13996 s.select_ranges([
13997 Point::new(1, 11)..Point::new(1, 11),
13998 Point::new(7, 11)..Point::new(7, 11),
13999 ])
14000 });
14001
14002 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14003 });
14004
14005 editor.update_in(cx, |editor, window, cx| {
14006 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14007 });
14008
14009 fake_server
14010 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14011 let completion_item = lsp::CompletionItem {
14012 label: "saturating_sub()".into(),
14013 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14014 lsp::InsertReplaceEdit {
14015 new_text: "saturating_sub()".to_owned(),
14016 insert: lsp::Range::new(
14017 lsp::Position::new(7, 7),
14018 lsp::Position::new(7, 11),
14019 ),
14020 replace: lsp::Range::new(
14021 lsp::Position::new(7, 7),
14022 lsp::Position::new(7, 13),
14023 ),
14024 },
14025 )),
14026 ..lsp::CompletionItem::default()
14027 };
14028
14029 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14030 })
14031 .next()
14032 .await
14033 .unwrap();
14034
14035 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14036 .await;
14037
14038 editor
14039 .update_in(cx, |editor, window, cx| {
14040 editor
14041 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14042 .unwrap()
14043 })
14044 .await
14045 .unwrap();
14046
14047 editor.update(cx, |editor, cx| {
14048 assert_text_with_selections(editor, expected_multibuffer, cx);
14049 })
14050}
14051
14052#[gpui::test]
14053async fn test_completion(cx: &mut TestAppContext) {
14054 init_test(cx, |_| {});
14055
14056 let mut cx = EditorLspTestContext::new_rust(
14057 lsp::ServerCapabilities {
14058 completion_provider: Some(lsp::CompletionOptions {
14059 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14060 resolve_provider: Some(true),
14061 ..Default::default()
14062 }),
14063 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14064 ..Default::default()
14065 },
14066 cx,
14067 )
14068 .await;
14069 let counter = Arc::new(AtomicUsize::new(0));
14070
14071 cx.set_state(indoc! {"
14072 oneˇ
14073 two
14074 three
14075 "});
14076 cx.simulate_keystroke(".");
14077 handle_completion_request(
14078 indoc! {"
14079 one.|<>
14080 two
14081 three
14082 "},
14083 vec!["first_completion", "second_completion"],
14084 true,
14085 counter.clone(),
14086 &mut cx,
14087 )
14088 .await;
14089 cx.condition(|editor, _| editor.context_menu_visible())
14090 .await;
14091 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14092
14093 let _handler = handle_signature_help_request(
14094 &mut cx,
14095 lsp::SignatureHelp {
14096 signatures: vec![lsp::SignatureInformation {
14097 label: "test signature".to_string(),
14098 documentation: None,
14099 parameters: Some(vec![lsp::ParameterInformation {
14100 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14101 documentation: None,
14102 }]),
14103 active_parameter: None,
14104 }],
14105 active_signature: None,
14106 active_parameter: None,
14107 },
14108 );
14109 cx.update_editor(|editor, window, cx| {
14110 assert!(
14111 !editor.signature_help_state.is_shown(),
14112 "No signature help was called for"
14113 );
14114 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14115 });
14116 cx.run_until_parked();
14117 cx.update_editor(|editor, _, _| {
14118 assert!(
14119 !editor.signature_help_state.is_shown(),
14120 "No signature help should be shown when completions menu is open"
14121 );
14122 });
14123
14124 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14125 editor.context_menu_next(&Default::default(), window, cx);
14126 editor
14127 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14128 .unwrap()
14129 });
14130 cx.assert_editor_state(indoc! {"
14131 one.second_completionˇ
14132 two
14133 three
14134 "});
14135
14136 handle_resolve_completion_request(
14137 &mut cx,
14138 Some(vec![
14139 (
14140 //This overlaps with the primary completion edit which is
14141 //misbehavior from the LSP spec, test that we filter it out
14142 indoc! {"
14143 one.second_ˇcompletion
14144 two
14145 threeˇ
14146 "},
14147 "overlapping additional edit",
14148 ),
14149 (
14150 indoc! {"
14151 one.second_completion
14152 two
14153 threeˇ
14154 "},
14155 "\nadditional edit",
14156 ),
14157 ]),
14158 )
14159 .await;
14160 apply_additional_edits.await.unwrap();
14161 cx.assert_editor_state(indoc! {"
14162 one.second_completionˇ
14163 two
14164 three
14165 additional edit
14166 "});
14167
14168 cx.set_state(indoc! {"
14169 one.second_completion
14170 twoˇ
14171 threeˇ
14172 additional edit
14173 "});
14174 cx.simulate_keystroke(" ");
14175 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14176 cx.simulate_keystroke("s");
14177 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14178
14179 cx.assert_editor_state(indoc! {"
14180 one.second_completion
14181 two sˇ
14182 three sˇ
14183 additional edit
14184 "});
14185 handle_completion_request(
14186 indoc! {"
14187 one.second_completion
14188 two s
14189 three <s|>
14190 additional edit
14191 "},
14192 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14193 true,
14194 counter.clone(),
14195 &mut cx,
14196 )
14197 .await;
14198 cx.condition(|editor, _| editor.context_menu_visible())
14199 .await;
14200 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14201
14202 cx.simulate_keystroke("i");
14203
14204 handle_completion_request(
14205 indoc! {"
14206 one.second_completion
14207 two si
14208 three <si|>
14209 additional edit
14210 "},
14211 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14212 true,
14213 counter.clone(),
14214 &mut cx,
14215 )
14216 .await;
14217 cx.condition(|editor, _| editor.context_menu_visible())
14218 .await;
14219 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14220
14221 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14222 editor
14223 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14224 .unwrap()
14225 });
14226 cx.assert_editor_state(indoc! {"
14227 one.second_completion
14228 two sixth_completionˇ
14229 three sixth_completionˇ
14230 additional edit
14231 "});
14232
14233 apply_additional_edits.await.unwrap();
14234
14235 update_test_language_settings(&mut cx, |settings| {
14236 settings.defaults.show_completions_on_input = Some(false);
14237 });
14238 cx.set_state("editorˇ");
14239 cx.simulate_keystroke(".");
14240 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14241 cx.simulate_keystrokes("c l o");
14242 cx.assert_editor_state("editor.cloˇ");
14243 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14244 cx.update_editor(|editor, window, cx| {
14245 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14246 });
14247 handle_completion_request(
14248 "editor.<clo|>",
14249 vec!["close", "clobber"],
14250 true,
14251 counter.clone(),
14252 &mut cx,
14253 )
14254 .await;
14255 cx.condition(|editor, _| editor.context_menu_visible())
14256 .await;
14257 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14258
14259 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14260 editor
14261 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14262 .unwrap()
14263 });
14264 cx.assert_editor_state("editor.clobberˇ");
14265 handle_resolve_completion_request(&mut cx, None).await;
14266 apply_additional_edits.await.unwrap();
14267}
14268
14269#[gpui::test]
14270async fn test_completion_reuse(cx: &mut TestAppContext) {
14271 init_test(cx, |_| {});
14272
14273 let mut cx = EditorLspTestContext::new_rust(
14274 lsp::ServerCapabilities {
14275 completion_provider: Some(lsp::CompletionOptions {
14276 trigger_characters: Some(vec![".".to_string()]),
14277 ..Default::default()
14278 }),
14279 ..Default::default()
14280 },
14281 cx,
14282 )
14283 .await;
14284
14285 let counter = Arc::new(AtomicUsize::new(0));
14286 cx.set_state("objˇ");
14287 cx.simulate_keystroke(".");
14288
14289 // Initial completion request returns complete results
14290 let is_incomplete = false;
14291 handle_completion_request(
14292 "obj.|<>",
14293 vec!["a", "ab", "abc"],
14294 is_incomplete,
14295 counter.clone(),
14296 &mut cx,
14297 )
14298 .await;
14299 cx.run_until_parked();
14300 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14301 cx.assert_editor_state("obj.ˇ");
14302 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14303
14304 // Type "a" - filters existing completions
14305 cx.simulate_keystroke("a");
14306 cx.run_until_parked();
14307 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14308 cx.assert_editor_state("obj.aˇ");
14309 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14310
14311 // Type "b" - filters existing completions
14312 cx.simulate_keystroke("b");
14313 cx.run_until_parked();
14314 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14315 cx.assert_editor_state("obj.abˇ");
14316 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14317
14318 // Type "c" - filters existing completions
14319 cx.simulate_keystroke("c");
14320 cx.run_until_parked();
14321 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14322 cx.assert_editor_state("obj.abcˇ");
14323 check_displayed_completions(vec!["abc"], &mut cx);
14324
14325 // Backspace to delete "c" - filters existing completions
14326 cx.update_editor(|editor, window, cx| {
14327 editor.backspace(&Backspace, window, cx);
14328 });
14329 cx.run_until_parked();
14330 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14331 cx.assert_editor_state("obj.abˇ");
14332 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14333
14334 // Moving cursor to the left dismisses menu.
14335 cx.update_editor(|editor, window, cx| {
14336 editor.move_left(&MoveLeft, window, cx);
14337 });
14338 cx.run_until_parked();
14339 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14340 cx.assert_editor_state("obj.aˇb");
14341 cx.update_editor(|editor, _, _| {
14342 assert_eq!(editor.context_menu_visible(), false);
14343 });
14344
14345 // Type "b" - new request
14346 cx.simulate_keystroke("b");
14347 let is_incomplete = false;
14348 handle_completion_request(
14349 "obj.<ab|>a",
14350 vec!["ab", "abc"],
14351 is_incomplete,
14352 counter.clone(),
14353 &mut cx,
14354 )
14355 .await;
14356 cx.run_until_parked();
14357 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14358 cx.assert_editor_state("obj.abˇb");
14359 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14360
14361 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14362 cx.update_editor(|editor, window, cx| {
14363 editor.backspace(&Backspace, window, cx);
14364 });
14365 let is_incomplete = false;
14366 handle_completion_request(
14367 "obj.<a|>b",
14368 vec!["a", "ab", "abc"],
14369 is_incomplete,
14370 counter.clone(),
14371 &mut cx,
14372 )
14373 .await;
14374 cx.run_until_parked();
14375 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14376 cx.assert_editor_state("obj.aˇb");
14377 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14378
14379 // Backspace to delete "a" - dismisses menu.
14380 cx.update_editor(|editor, window, cx| {
14381 editor.backspace(&Backspace, window, cx);
14382 });
14383 cx.run_until_parked();
14384 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14385 cx.assert_editor_state("obj.ˇb");
14386 cx.update_editor(|editor, _, _| {
14387 assert_eq!(editor.context_menu_visible(), false);
14388 });
14389}
14390
14391#[gpui::test]
14392async fn test_word_completion(cx: &mut TestAppContext) {
14393 let lsp_fetch_timeout_ms = 10;
14394 init_test(cx, |language_settings| {
14395 language_settings.defaults.completions = Some(CompletionSettingsContent {
14396 words_min_length: Some(0),
14397 lsp_fetch_timeout_ms: Some(10),
14398 lsp_insert_mode: Some(LspInsertMode::Insert),
14399 ..Default::default()
14400 });
14401 });
14402
14403 let mut cx = EditorLspTestContext::new_rust(
14404 lsp::ServerCapabilities {
14405 completion_provider: Some(lsp::CompletionOptions {
14406 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14407 ..lsp::CompletionOptions::default()
14408 }),
14409 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14410 ..lsp::ServerCapabilities::default()
14411 },
14412 cx,
14413 )
14414 .await;
14415
14416 let throttle_completions = Arc::new(AtomicBool::new(false));
14417
14418 let lsp_throttle_completions = throttle_completions.clone();
14419 let _completion_requests_handler =
14420 cx.lsp
14421 .server
14422 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14423 let lsp_throttle_completions = lsp_throttle_completions.clone();
14424 let cx = cx.clone();
14425 async move {
14426 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14427 cx.background_executor()
14428 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14429 .await;
14430 }
14431 Ok(Some(lsp::CompletionResponse::Array(vec![
14432 lsp::CompletionItem {
14433 label: "first".into(),
14434 ..lsp::CompletionItem::default()
14435 },
14436 lsp::CompletionItem {
14437 label: "last".into(),
14438 ..lsp::CompletionItem::default()
14439 },
14440 ])))
14441 }
14442 });
14443
14444 cx.set_state(indoc! {"
14445 oneˇ
14446 two
14447 three
14448 "});
14449 cx.simulate_keystroke(".");
14450 cx.executor().run_until_parked();
14451 cx.condition(|editor, _| editor.context_menu_visible())
14452 .await;
14453 cx.update_editor(|editor, window, cx| {
14454 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14455 {
14456 assert_eq!(
14457 completion_menu_entries(menu),
14458 &["first", "last"],
14459 "When LSP server is fast to reply, no fallback word completions are used"
14460 );
14461 } else {
14462 panic!("expected completion menu to be open");
14463 }
14464 editor.cancel(&Cancel, window, cx);
14465 });
14466 cx.executor().run_until_parked();
14467 cx.condition(|editor, _| !editor.context_menu_visible())
14468 .await;
14469
14470 throttle_completions.store(true, atomic::Ordering::Release);
14471 cx.simulate_keystroke(".");
14472 cx.executor()
14473 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14474 cx.executor().run_until_parked();
14475 cx.condition(|editor, _| editor.context_menu_visible())
14476 .await;
14477 cx.update_editor(|editor, _, _| {
14478 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14479 {
14480 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14481 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14482 } else {
14483 panic!("expected completion menu to be open");
14484 }
14485 });
14486}
14487
14488#[gpui::test]
14489async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14490 init_test(cx, |language_settings| {
14491 language_settings.defaults.completions = Some(CompletionSettingsContent {
14492 words: Some(WordsCompletionMode::Enabled),
14493 words_min_length: Some(0),
14494 lsp_insert_mode: Some(LspInsertMode::Insert),
14495 ..Default::default()
14496 });
14497 });
14498
14499 let mut cx = EditorLspTestContext::new_rust(
14500 lsp::ServerCapabilities {
14501 completion_provider: Some(lsp::CompletionOptions {
14502 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14503 ..lsp::CompletionOptions::default()
14504 }),
14505 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14506 ..lsp::ServerCapabilities::default()
14507 },
14508 cx,
14509 )
14510 .await;
14511
14512 let _completion_requests_handler =
14513 cx.lsp
14514 .server
14515 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14516 Ok(Some(lsp::CompletionResponse::Array(vec![
14517 lsp::CompletionItem {
14518 label: "first".into(),
14519 ..lsp::CompletionItem::default()
14520 },
14521 lsp::CompletionItem {
14522 label: "last".into(),
14523 ..lsp::CompletionItem::default()
14524 },
14525 ])))
14526 });
14527
14528 cx.set_state(indoc! {"ˇ
14529 first
14530 last
14531 second
14532 "});
14533 cx.simulate_keystroke(".");
14534 cx.executor().run_until_parked();
14535 cx.condition(|editor, _| editor.context_menu_visible())
14536 .await;
14537 cx.update_editor(|editor, _, _| {
14538 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14539 {
14540 assert_eq!(
14541 completion_menu_entries(menu),
14542 &["first", "last", "second"],
14543 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14544 );
14545 } else {
14546 panic!("expected completion menu to be open");
14547 }
14548 });
14549}
14550
14551#[gpui::test]
14552async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14553 init_test(cx, |language_settings| {
14554 language_settings.defaults.completions = Some(CompletionSettingsContent {
14555 words: Some(WordsCompletionMode::Disabled),
14556 words_min_length: Some(0),
14557 lsp_insert_mode: Some(LspInsertMode::Insert),
14558 ..Default::default()
14559 });
14560 });
14561
14562 let mut cx = EditorLspTestContext::new_rust(
14563 lsp::ServerCapabilities {
14564 completion_provider: Some(lsp::CompletionOptions {
14565 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14566 ..lsp::CompletionOptions::default()
14567 }),
14568 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14569 ..lsp::ServerCapabilities::default()
14570 },
14571 cx,
14572 )
14573 .await;
14574
14575 let _completion_requests_handler =
14576 cx.lsp
14577 .server
14578 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14579 panic!("LSP completions should not be queried when dealing with word completions")
14580 });
14581
14582 cx.set_state(indoc! {"ˇ
14583 first
14584 last
14585 second
14586 "});
14587 cx.update_editor(|editor, window, cx| {
14588 editor.show_word_completions(&ShowWordCompletions, window, cx);
14589 });
14590 cx.executor().run_until_parked();
14591 cx.condition(|editor, _| editor.context_menu_visible())
14592 .await;
14593 cx.update_editor(|editor, _, _| {
14594 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14595 {
14596 assert_eq!(
14597 completion_menu_entries(menu),
14598 &["first", "last", "second"],
14599 "`ShowWordCompletions` action should show word completions"
14600 );
14601 } else {
14602 panic!("expected completion menu to be open");
14603 }
14604 });
14605
14606 cx.simulate_keystroke("l");
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 &["last"],
14616 "After showing word completions, further editing should filter them and not query the LSP"
14617 );
14618 } else {
14619 panic!("expected completion menu to be open");
14620 }
14621 });
14622}
14623
14624#[gpui::test]
14625async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14626 init_test(cx, |language_settings| {
14627 language_settings.defaults.completions = Some(CompletionSettingsContent {
14628 words_min_length: Some(0),
14629 lsp: Some(false),
14630 lsp_insert_mode: Some(LspInsertMode::Insert),
14631 ..Default::default()
14632 });
14633 });
14634
14635 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14636
14637 cx.set_state(indoc! {"ˇ
14638 0_usize
14639 let
14640 33
14641 4.5f32
14642 "});
14643 cx.update_editor(|editor, window, cx| {
14644 editor.show_completions(&ShowCompletions::default(), window, cx);
14645 });
14646 cx.executor().run_until_parked();
14647 cx.condition(|editor, _| editor.context_menu_visible())
14648 .await;
14649 cx.update_editor(|editor, window, cx| {
14650 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14651 {
14652 assert_eq!(
14653 completion_menu_entries(menu),
14654 &["let"],
14655 "With no digits in the completion query, no digits should be in the word completions"
14656 );
14657 } else {
14658 panic!("expected completion menu to be open");
14659 }
14660 editor.cancel(&Cancel, window, cx);
14661 });
14662
14663 cx.set_state(indoc! {"3ˇ
14664 0_usize
14665 let
14666 3
14667 33.35f32
14668 "});
14669 cx.update_editor(|editor, window, cx| {
14670 editor.show_completions(&ShowCompletions::default(), window, cx);
14671 });
14672 cx.executor().run_until_parked();
14673 cx.condition(|editor, _| editor.context_menu_visible())
14674 .await;
14675 cx.update_editor(|editor, _, _| {
14676 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14677 {
14678 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14679 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14680 } else {
14681 panic!("expected completion menu to be open");
14682 }
14683 });
14684}
14685
14686#[gpui::test]
14687async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14688 init_test(cx, |language_settings| {
14689 language_settings.defaults.completions = Some(CompletionSettingsContent {
14690 words: Some(WordsCompletionMode::Enabled),
14691 words_min_length: Some(3),
14692 lsp_insert_mode: Some(LspInsertMode::Insert),
14693 ..Default::default()
14694 });
14695 });
14696
14697 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14698 cx.set_state(indoc! {"ˇ
14699 wow
14700 wowen
14701 wowser
14702 "});
14703 cx.simulate_keystroke("w");
14704 cx.executor().run_until_parked();
14705 cx.update_editor(|editor, _, _| {
14706 if editor.context_menu.borrow_mut().is_some() {
14707 panic!(
14708 "expected completion menu to be hidden, as words completion threshold is not met"
14709 );
14710 }
14711 });
14712
14713 cx.update_editor(|editor, window, cx| {
14714 editor.show_word_completions(&ShowWordCompletions, window, cx);
14715 });
14716 cx.executor().run_until_parked();
14717 cx.update_editor(|editor, window, cx| {
14718 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14719 {
14720 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");
14721 } else {
14722 panic!("expected completion menu to be open after the word completions are called with an action");
14723 }
14724
14725 editor.cancel(&Cancel, window, cx);
14726 });
14727 cx.update_editor(|editor, _, _| {
14728 if editor.context_menu.borrow_mut().is_some() {
14729 panic!("expected completion menu to be hidden after canceling");
14730 }
14731 });
14732
14733 cx.simulate_keystroke("o");
14734 cx.executor().run_until_parked();
14735 cx.update_editor(|editor, _, _| {
14736 if editor.context_menu.borrow_mut().is_some() {
14737 panic!(
14738 "expected completion menu to be hidden, as words completion threshold is not met still"
14739 );
14740 }
14741 });
14742
14743 cx.simulate_keystroke("w");
14744 cx.executor().run_until_parked();
14745 cx.update_editor(|editor, _, _| {
14746 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14747 {
14748 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14749 } else {
14750 panic!("expected completion menu to be open after the word completions threshold is met");
14751 }
14752 });
14753}
14754
14755#[gpui::test]
14756async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14757 init_test(cx, |language_settings| {
14758 language_settings.defaults.completions = Some(CompletionSettingsContent {
14759 words: Some(WordsCompletionMode::Enabled),
14760 words_min_length: Some(0),
14761 lsp_insert_mode: Some(LspInsertMode::Insert),
14762 ..Default::default()
14763 });
14764 });
14765
14766 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14767 cx.update_editor(|editor, _, _| {
14768 editor.disable_word_completions();
14769 });
14770 cx.set_state(indoc! {"ˇ
14771 wow
14772 wowen
14773 wowser
14774 "});
14775 cx.simulate_keystroke("w");
14776 cx.executor().run_until_parked();
14777 cx.update_editor(|editor, _, _| {
14778 if editor.context_menu.borrow_mut().is_some() {
14779 panic!(
14780 "expected completion menu to be hidden, as words completion are disabled for this editor"
14781 );
14782 }
14783 });
14784
14785 cx.update_editor(|editor, window, cx| {
14786 editor.show_word_completions(&ShowWordCompletions, window, cx);
14787 });
14788 cx.executor().run_until_parked();
14789 cx.update_editor(|editor, _, _| {
14790 if editor.context_menu.borrow_mut().is_some() {
14791 panic!(
14792 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14793 );
14794 }
14795 });
14796}
14797
14798fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14799 let position = || lsp::Position {
14800 line: params.text_document_position.position.line,
14801 character: params.text_document_position.position.character,
14802 };
14803 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14804 range: lsp::Range {
14805 start: position(),
14806 end: position(),
14807 },
14808 new_text: text.to_string(),
14809 }))
14810}
14811
14812#[gpui::test]
14813async fn test_multiline_completion(cx: &mut TestAppContext) {
14814 init_test(cx, |_| {});
14815
14816 let fs = FakeFs::new(cx.executor());
14817 fs.insert_tree(
14818 path!("/a"),
14819 json!({
14820 "main.ts": "a",
14821 }),
14822 )
14823 .await;
14824
14825 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14826 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14827 let typescript_language = Arc::new(Language::new(
14828 LanguageConfig {
14829 name: "TypeScript".into(),
14830 matcher: LanguageMatcher {
14831 path_suffixes: vec!["ts".to_string()],
14832 ..LanguageMatcher::default()
14833 },
14834 line_comments: vec!["// ".into()],
14835 ..LanguageConfig::default()
14836 },
14837 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14838 ));
14839 language_registry.add(typescript_language.clone());
14840 let mut fake_servers = language_registry.register_fake_lsp(
14841 "TypeScript",
14842 FakeLspAdapter {
14843 capabilities: lsp::ServerCapabilities {
14844 completion_provider: Some(lsp::CompletionOptions {
14845 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14846 ..lsp::CompletionOptions::default()
14847 }),
14848 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14849 ..lsp::ServerCapabilities::default()
14850 },
14851 // Emulate vtsls label generation
14852 label_for_completion: Some(Box::new(|item, _| {
14853 let text = if let Some(description) = item
14854 .label_details
14855 .as_ref()
14856 .and_then(|label_details| label_details.description.as_ref())
14857 {
14858 format!("{} {}", item.label, description)
14859 } else if let Some(detail) = &item.detail {
14860 format!("{} {}", item.label, detail)
14861 } else {
14862 item.label.clone()
14863 };
14864 let len = text.len();
14865 Some(language::CodeLabel {
14866 text,
14867 runs: Vec::new(),
14868 filter_range: 0..len,
14869 })
14870 })),
14871 ..FakeLspAdapter::default()
14872 },
14873 );
14874 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14875 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14876 let worktree_id = workspace
14877 .update(cx, |workspace, _window, cx| {
14878 workspace.project().update(cx, |project, cx| {
14879 project.worktrees(cx).next().unwrap().read(cx).id()
14880 })
14881 })
14882 .unwrap();
14883 let _buffer = project
14884 .update(cx, |project, cx| {
14885 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14886 })
14887 .await
14888 .unwrap();
14889 let editor = workspace
14890 .update(cx, |workspace, window, cx| {
14891 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
14892 })
14893 .unwrap()
14894 .await
14895 .unwrap()
14896 .downcast::<Editor>()
14897 .unwrap();
14898 let fake_server = fake_servers.next().await.unwrap();
14899
14900 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14901 let multiline_label_2 = "a\nb\nc\n";
14902 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14903 let multiline_description = "d\ne\nf\n";
14904 let multiline_detail_2 = "g\nh\ni\n";
14905
14906 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14907 move |params, _| async move {
14908 Ok(Some(lsp::CompletionResponse::Array(vec![
14909 lsp::CompletionItem {
14910 label: multiline_label.to_string(),
14911 text_edit: gen_text_edit(¶ms, "new_text_1"),
14912 ..lsp::CompletionItem::default()
14913 },
14914 lsp::CompletionItem {
14915 label: "single line label 1".to_string(),
14916 detail: Some(multiline_detail.to_string()),
14917 text_edit: gen_text_edit(¶ms, "new_text_2"),
14918 ..lsp::CompletionItem::default()
14919 },
14920 lsp::CompletionItem {
14921 label: "single line label 2".to_string(),
14922 label_details: Some(lsp::CompletionItemLabelDetails {
14923 description: Some(multiline_description.to_string()),
14924 detail: None,
14925 }),
14926 text_edit: gen_text_edit(¶ms, "new_text_2"),
14927 ..lsp::CompletionItem::default()
14928 },
14929 lsp::CompletionItem {
14930 label: multiline_label_2.to_string(),
14931 detail: Some(multiline_detail_2.to_string()),
14932 text_edit: gen_text_edit(¶ms, "new_text_3"),
14933 ..lsp::CompletionItem::default()
14934 },
14935 lsp::CompletionItem {
14936 label: "Label with many spaces and \t but without newlines".to_string(),
14937 detail: Some(
14938 "Details with many spaces and \t but without newlines".to_string(),
14939 ),
14940 text_edit: gen_text_edit(¶ms, "new_text_4"),
14941 ..lsp::CompletionItem::default()
14942 },
14943 ])))
14944 },
14945 );
14946
14947 editor.update_in(cx, |editor, window, cx| {
14948 cx.focus_self(window);
14949 editor.move_to_end(&MoveToEnd, window, cx);
14950 editor.handle_input(".", window, cx);
14951 });
14952 cx.run_until_parked();
14953 completion_handle.next().await.unwrap();
14954
14955 editor.update(cx, |editor, _| {
14956 assert!(editor.context_menu_visible());
14957 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14958 {
14959 let completion_labels = menu
14960 .completions
14961 .borrow()
14962 .iter()
14963 .map(|c| c.label.text.clone())
14964 .collect::<Vec<_>>();
14965 assert_eq!(
14966 completion_labels,
14967 &[
14968 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14969 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14970 "single line label 2 d e f ",
14971 "a b c g h i ",
14972 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14973 ],
14974 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14975 );
14976
14977 for completion in menu
14978 .completions
14979 .borrow()
14980 .iter() {
14981 assert_eq!(
14982 completion.label.filter_range,
14983 0..completion.label.text.len(),
14984 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14985 );
14986 }
14987 } else {
14988 panic!("expected completion menu to be open");
14989 }
14990 });
14991}
14992
14993#[gpui::test]
14994async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14995 init_test(cx, |_| {});
14996 let mut cx = EditorLspTestContext::new_rust(
14997 lsp::ServerCapabilities {
14998 completion_provider: Some(lsp::CompletionOptions {
14999 trigger_characters: Some(vec![".".to_string()]),
15000 ..Default::default()
15001 }),
15002 ..Default::default()
15003 },
15004 cx,
15005 )
15006 .await;
15007 cx.lsp
15008 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15009 Ok(Some(lsp::CompletionResponse::Array(vec![
15010 lsp::CompletionItem {
15011 label: "first".into(),
15012 ..Default::default()
15013 },
15014 lsp::CompletionItem {
15015 label: "last".into(),
15016 ..Default::default()
15017 },
15018 ])))
15019 });
15020 cx.set_state("variableˇ");
15021 cx.simulate_keystroke(".");
15022 cx.executor().run_until_parked();
15023
15024 cx.update_editor(|editor, _, _| {
15025 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15026 {
15027 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15028 } else {
15029 panic!("expected completion menu to be open");
15030 }
15031 });
15032
15033 cx.update_editor(|editor, window, cx| {
15034 editor.move_page_down(&MovePageDown::default(), window, cx);
15035 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15036 {
15037 assert!(
15038 menu.selected_item == 1,
15039 "expected PageDown to select the last item from the context menu"
15040 );
15041 } else {
15042 panic!("expected completion menu to stay open after PageDown");
15043 }
15044 });
15045
15046 cx.update_editor(|editor, window, cx| {
15047 editor.move_page_up(&MovePageUp::default(), window, cx);
15048 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15049 {
15050 assert!(
15051 menu.selected_item == 0,
15052 "expected PageUp to select the first item from the context menu"
15053 );
15054 } else {
15055 panic!("expected completion menu to stay open after PageUp");
15056 }
15057 });
15058}
15059
15060#[gpui::test]
15061async fn test_as_is_completions(cx: &mut TestAppContext) {
15062 init_test(cx, |_| {});
15063 let mut cx = EditorLspTestContext::new_rust(
15064 lsp::ServerCapabilities {
15065 completion_provider: Some(lsp::CompletionOptions {
15066 ..Default::default()
15067 }),
15068 ..Default::default()
15069 },
15070 cx,
15071 )
15072 .await;
15073 cx.lsp
15074 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15075 Ok(Some(lsp::CompletionResponse::Array(vec![
15076 lsp::CompletionItem {
15077 label: "unsafe".into(),
15078 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15079 range: lsp::Range {
15080 start: lsp::Position {
15081 line: 1,
15082 character: 2,
15083 },
15084 end: lsp::Position {
15085 line: 1,
15086 character: 3,
15087 },
15088 },
15089 new_text: "unsafe".to_string(),
15090 })),
15091 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15092 ..Default::default()
15093 },
15094 ])))
15095 });
15096 cx.set_state("fn a() {}\n nˇ");
15097 cx.executor().run_until_parked();
15098 cx.update_editor(|editor, window, cx| {
15099 editor.show_completions(
15100 &ShowCompletions {
15101 trigger: Some("\n".into()),
15102 },
15103 window,
15104 cx,
15105 );
15106 });
15107 cx.executor().run_until_parked();
15108
15109 cx.update_editor(|editor, window, cx| {
15110 editor.confirm_completion(&Default::default(), window, cx)
15111 });
15112 cx.executor().run_until_parked();
15113 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15114}
15115
15116#[gpui::test]
15117async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15118 init_test(cx, |_| {});
15119 let language =
15120 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15121 let mut cx = EditorLspTestContext::new(
15122 language,
15123 lsp::ServerCapabilities {
15124 completion_provider: Some(lsp::CompletionOptions {
15125 ..lsp::CompletionOptions::default()
15126 }),
15127 ..lsp::ServerCapabilities::default()
15128 },
15129 cx,
15130 )
15131 .await;
15132
15133 cx.set_state(
15134 "#ifndef BAR_H
15135#define BAR_H
15136
15137#include <stdbool.h>
15138
15139int fn_branch(bool do_branch1, bool do_branch2);
15140
15141#endif // BAR_H
15142ˇ",
15143 );
15144 cx.executor().run_until_parked();
15145 cx.update_editor(|editor, window, cx| {
15146 editor.handle_input("#", window, cx);
15147 });
15148 cx.executor().run_until_parked();
15149 cx.update_editor(|editor, window, cx| {
15150 editor.handle_input("i", window, cx);
15151 });
15152 cx.executor().run_until_parked();
15153 cx.update_editor(|editor, window, cx| {
15154 editor.handle_input("n", window, cx);
15155 });
15156 cx.executor().run_until_parked();
15157 cx.assert_editor_state(
15158 "#ifndef BAR_H
15159#define BAR_H
15160
15161#include <stdbool.h>
15162
15163int fn_branch(bool do_branch1, bool do_branch2);
15164
15165#endif // BAR_H
15166#inˇ",
15167 );
15168
15169 cx.lsp
15170 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15171 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15172 is_incomplete: false,
15173 item_defaults: None,
15174 items: vec![lsp::CompletionItem {
15175 kind: Some(lsp::CompletionItemKind::SNIPPET),
15176 label_details: Some(lsp::CompletionItemLabelDetails {
15177 detail: Some("header".to_string()),
15178 description: None,
15179 }),
15180 label: " include".to_string(),
15181 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15182 range: lsp::Range {
15183 start: lsp::Position {
15184 line: 8,
15185 character: 1,
15186 },
15187 end: lsp::Position {
15188 line: 8,
15189 character: 1,
15190 },
15191 },
15192 new_text: "include \"$0\"".to_string(),
15193 })),
15194 sort_text: Some("40b67681include".to_string()),
15195 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15196 filter_text: Some("include".to_string()),
15197 insert_text: Some("include \"$0\"".to_string()),
15198 ..lsp::CompletionItem::default()
15199 }],
15200 })))
15201 });
15202 cx.update_editor(|editor, window, cx| {
15203 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15204 });
15205 cx.executor().run_until_parked();
15206 cx.update_editor(|editor, window, cx| {
15207 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15208 });
15209 cx.executor().run_until_parked();
15210 cx.assert_editor_state(
15211 "#ifndef BAR_H
15212#define BAR_H
15213
15214#include <stdbool.h>
15215
15216int fn_branch(bool do_branch1, bool do_branch2);
15217
15218#endif // BAR_H
15219#include \"ˇ\"",
15220 );
15221
15222 cx.lsp
15223 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15224 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15225 is_incomplete: true,
15226 item_defaults: None,
15227 items: vec![lsp::CompletionItem {
15228 kind: Some(lsp::CompletionItemKind::FILE),
15229 label: "AGL/".to_string(),
15230 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15231 range: lsp::Range {
15232 start: lsp::Position {
15233 line: 8,
15234 character: 10,
15235 },
15236 end: lsp::Position {
15237 line: 8,
15238 character: 11,
15239 },
15240 },
15241 new_text: "AGL/".to_string(),
15242 })),
15243 sort_text: Some("40b67681AGL/".to_string()),
15244 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15245 filter_text: Some("AGL/".to_string()),
15246 insert_text: Some("AGL/".to_string()),
15247 ..lsp::CompletionItem::default()
15248 }],
15249 })))
15250 });
15251 cx.update_editor(|editor, window, cx| {
15252 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15253 });
15254 cx.executor().run_until_parked();
15255 cx.update_editor(|editor, window, cx| {
15256 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15257 });
15258 cx.executor().run_until_parked();
15259 cx.assert_editor_state(
15260 r##"#ifndef BAR_H
15261#define BAR_H
15262
15263#include <stdbool.h>
15264
15265int fn_branch(bool do_branch1, bool do_branch2);
15266
15267#endif // BAR_H
15268#include "AGL/ˇ"##,
15269 );
15270
15271 cx.update_editor(|editor, window, cx| {
15272 editor.handle_input("\"", window, cx);
15273 });
15274 cx.executor().run_until_parked();
15275 cx.assert_editor_state(
15276 r##"#ifndef BAR_H
15277#define BAR_H
15278
15279#include <stdbool.h>
15280
15281int fn_branch(bool do_branch1, bool do_branch2);
15282
15283#endif // BAR_H
15284#include "AGL/"ˇ"##,
15285 );
15286}
15287
15288#[gpui::test]
15289async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15290 init_test(cx, |_| {});
15291
15292 let mut cx = EditorLspTestContext::new_rust(
15293 lsp::ServerCapabilities {
15294 completion_provider: Some(lsp::CompletionOptions {
15295 trigger_characters: Some(vec![".".to_string()]),
15296 resolve_provider: Some(true),
15297 ..Default::default()
15298 }),
15299 ..Default::default()
15300 },
15301 cx,
15302 )
15303 .await;
15304
15305 cx.set_state("fn main() { let a = 2ˇ; }");
15306 cx.simulate_keystroke(".");
15307 let completion_item = lsp::CompletionItem {
15308 label: "Some".into(),
15309 kind: Some(lsp::CompletionItemKind::SNIPPET),
15310 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15311 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15312 kind: lsp::MarkupKind::Markdown,
15313 value: "```rust\nSome(2)\n```".to_string(),
15314 })),
15315 deprecated: Some(false),
15316 sort_text: Some("Some".to_string()),
15317 filter_text: Some("Some".to_string()),
15318 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15319 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15320 range: lsp::Range {
15321 start: lsp::Position {
15322 line: 0,
15323 character: 22,
15324 },
15325 end: lsp::Position {
15326 line: 0,
15327 character: 22,
15328 },
15329 },
15330 new_text: "Some(2)".to_string(),
15331 })),
15332 additional_text_edits: Some(vec![lsp::TextEdit {
15333 range: lsp::Range {
15334 start: lsp::Position {
15335 line: 0,
15336 character: 20,
15337 },
15338 end: lsp::Position {
15339 line: 0,
15340 character: 22,
15341 },
15342 },
15343 new_text: "".to_string(),
15344 }]),
15345 ..Default::default()
15346 };
15347
15348 let closure_completion_item = completion_item.clone();
15349 let counter = Arc::new(AtomicUsize::new(0));
15350 let counter_clone = counter.clone();
15351 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15352 let task_completion_item = closure_completion_item.clone();
15353 counter_clone.fetch_add(1, atomic::Ordering::Release);
15354 async move {
15355 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15356 is_incomplete: true,
15357 item_defaults: None,
15358 items: vec![task_completion_item],
15359 })))
15360 }
15361 });
15362
15363 cx.condition(|editor, _| editor.context_menu_visible())
15364 .await;
15365 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15366 assert!(request.next().await.is_some());
15367 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15368
15369 cx.simulate_keystrokes("S o m");
15370 cx.condition(|editor, _| editor.context_menu_visible())
15371 .await;
15372 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15373 assert!(request.next().await.is_some());
15374 assert!(request.next().await.is_some());
15375 assert!(request.next().await.is_some());
15376 request.close();
15377 assert!(request.next().await.is_none());
15378 assert_eq!(
15379 counter.load(atomic::Ordering::Acquire),
15380 4,
15381 "With the completions menu open, only one LSP request should happen per input"
15382 );
15383}
15384
15385#[gpui::test]
15386async fn test_toggle_comment(cx: &mut TestAppContext) {
15387 init_test(cx, |_| {});
15388 let mut cx = EditorTestContext::new(cx).await;
15389 let language = Arc::new(Language::new(
15390 LanguageConfig {
15391 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15392 ..Default::default()
15393 },
15394 Some(tree_sitter_rust::LANGUAGE.into()),
15395 ));
15396 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15397
15398 // If multiple selections intersect a line, the line is only toggled once.
15399 cx.set_state(indoc! {"
15400 fn a() {
15401 «//b();
15402 ˇ»// «c();
15403 //ˇ» d();
15404 }
15405 "});
15406
15407 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15408
15409 cx.assert_editor_state(indoc! {"
15410 fn a() {
15411 «b();
15412 c();
15413 ˇ» d();
15414 }
15415 "});
15416
15417 // The comment prefix is inserted at the same column for every line in a
15418 // selection.
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 // If a selection ends at the beginning of a line, that line is not toggled.
15430 cx.set_selections_state(indoc! {"
15431 fn a() {
15432 // b();
15433 «// c();
15434 ˇ» // d();
15435 }
15436 "});
15437
15438 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15439
15440 cx.assert_editor_state(indoc! {"
15441 fn a() {
15442 // b();
15443 «c();
15444 ˇ» // d();
15445 }
15446 "});
15447
15448 // If a selection span a single line and is empty, the line is toggled.
15449 cx.set_state(indoc! {"
15450 fn a() {
15451 a();
15452 b();
15453 ˇ
15454 }
15455 "});
15456
15457 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15458
15459 cx.assert_editor_state(indoc! {"
15460 fn a() {
15461 a();
15462 b();
15463 //•ˇ
15464 }
15465 "});
15466
15467 // If a selection span multiple lines, empty lines are not toggled.
15468 cx.set_state(indoc! {"
15469 fn a() {
15470 «a();
15471
15472 c();ˇ»
15473 }
15474 "});
15475
15476 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15477
15478 cx.assert_editor_state(indoc! {"
15479 fn a() {
15480 // «a();
15481
15482 // c();ˇ»
15483 }
15484 "});
15485
15486 // If a selection includes multiple comment prefixes, all lines are uncommented.
15487 cx.set_state(indoc! {"
15488 fn a() {
15489 «// a();
15490 /// b();
15491 //! c();ˇ»
15492 }
15493 "});
15494
15495 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15496
15497 cx.assert_editor_state(indoc! {"
15498 fn a() {
15499 «a();
15500 b();
15501 c();ˇ»
15502 }
15503 "});
15504}
15505
15506#[gpui::test]
15507async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15508 init_test(cx, |_| {});
15509 let mut cx = EditorTestContext::new(cx).await;
15510 let language = Arc::new(Language::new(
15511 LanguageConfig {
15512 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15513 ..Default::default()
15514 },
15515 Some(tree_sitter_rust::LANGUAGE.into()),
15516 ));
15517 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15518
15519 let toggle_comments = &ToggleComments {
15520 advance_downwards: false,
15521 ignore_indent: true,
15522 };
15523
15524 // If multiple selections intersect a line, the line is only toggled once.
15525 cx.set_state(indoc! {"
15526 fn a() {
15527 // «b();
15528 // c();
15529 // ˇ» d();
15530 }
15531 "});
15532
15533 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15534
15535 cx.assert_editor_state(indoc! {"
15536 fn a() {
15537 «b();
15538 c();
15539 ˇ» d();
15540 }
15541 "});
15542
15543 // The comment prefix is inserted at the beginning of each line
15544 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15545
15546 cx.assert_editor_state(indoc! {"
15547 fn a() {
15548 // «b();
15549 // c();
15550 // ˇ» d();
15551 }
15552 "});
15553
15554 // If a selection ends at the beginning of a line, that line is not toggled.
15555 cx.set_selections_state(indoc! {"
15556 fn a() {
15557 // b();
15558 // «c();
15559 ˇ»// d();
15560 }
15561 "});
15562
15563 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15564
15565 cx.assert_editor_state(indoc! {"
15566 fn a() {
15567 // b();
15568 «c();
15569 ˇ»// d();
15570 }
15571 "});
15572
15573 // If a selection span a single line and is empty, the line is toggled.
15574 cx.set_state(indoc! {"
15575 fn a() {
15576 a();
15577 b();
15578 ˇ
15579 }
15580 "});
15581
15582 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15583
15584 cx.assert_editor_state(indoc! {"
15585 fn a() {
15586 a();
15587 b();
15588 //ˇ
15589 }
15590 "});
15591
15592 // If a selection span multiple lines, empty lines are not toggled.
15593 cx.set_state(indoc! {"
15594 fn a() {
15595 «a();
15596
15597 c();ˇ»
15598 }
15599 "});
15600
15601 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15602
15603 cx.assert_editor_state(indoc! {"
15604 fn a() {
15605 // «a();
15606
15607 // c();ˇ»
15608 }
15609 "});
15610
15611 // If a selection includes multiple comment prefixes, all lines are uncommented.
15612 cx.set_state(indoc! {"
15613 fn a() {
15614 // «a();
15615 /// b();
15616 //! c();ˇ»
15617 }
15618 "});
15619
15620 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15621
15622 cx.assert_editor_state(indoc! {"
15623 fn a() {
15624 «a();
15625 b();
15626 c();ˇ»
15627 }
15628 "});
15629}
15630
15631#[gpui::test]
15632async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15633 init_test(cx, |_| {});
15634
15635 let language = Arc::new(Language::new(
15636 LanguageConfig {
15637 line_comments: vec!["// ".into()],
15638 ..Default::default()
15639 },
15640 Some(tree_sitter_rust::LANGUAGE.into()),
15641 ));
15642
15643 let mut cx = EditorTestContext::new(cx).await;
15644
15645 cx.language_registry().add(language.clone());
15646 cx.update_buffer(|buffer, cx| {
15647 buffer.set_language(Some(language), cx);
15648 });
15649
15650 let toggle_comments = &ToggleComments {
15651 advance_downwards: true,
15652 ignore_indent: false,
15653 };
15654
15655 // Single cursor on one line -> advance
15656 // Cursor moves horizontally 3 characters as well on non-blank line
15657 cx.set_state(indoc!(
15658 "fn a() {
15659 ˇdog();
15660 cat();
15661 }"
15662 ));
15663 cx.update_editor(|editor, window, cx| {
15664 editor.toggle_comments(toggle_comments, window, cx);
15665 });
15666 cx.assert_editor_state(indoc!(
15667 "fn a() {
15668 // dog();
15669 catˇ();
15670 }"
15671 ));
15672
15673 // Single selection on one line -> don't advance
15674 cx.set_state(indoc!(
15675 "fn a() {
15676 «dog()ˇ»;
15677 cat();
15678 }"
15679 ));
15680 cx.update_editor(|editor, window, cx| {
15681 editor.toggle_comments(toggle_comments, window, cx);
15682 });
15683 cx.assert_editor_state(indoc!(
15684 "fn a() {
15685 // «dog()ˇ»;
15686 cat();
15687 }"
15688 ));
15689
15690 // Multiple cursors on one line -> advance
15691 cx.set_state(indoc!(
15692 "fn a() {
15693 ˇdˇog();
15694 cat();
15695 }"
15696 ));
15697 cx.update_editor(|editor, window, cx| {
15698 editor.toggle_comments(toggle_comments, window, cx);
15699 });
15700 cx.assert_editor_state(indoc!(
15701 "fn a() {
15702 // dog();
15703 catˇ(ˇ);
15704 }"
15705 ));
15706
15707 // Multiple cursors on one line, with selection -> don't advance
15708 cx.set_state(indoc!(
15709 "fn a() {
15710 ˇdˇog«()ˇ»;
15711 cat();
15712 }"
15713 ));
15714 cx.update_editor(|editor, window, cx| {
15715 editor.toggle_comments(toggle_comments, window, cx);
15716 });
15717 cx.assert_editor_state(indoc!(
15718 "fn a() {
15719 // ˇdˇog«()ˇ»;
15720 cat();
15721 }"
15722 ));
15723
15724 // Single cursor on one line -> advance
15725 // Cursor moves to column 0 on blank line
15726 cx.set_state(indoc!(
15727 "fn a() {
15728 ˇdog();
15729
15730 cat();
15731 }"
15732 ));
15733 cx.update_editor(|editor, window, cx| {
15734 editor.toggle_comments(toggle_comments, window, cx);
15735 });
15736 cx.assert_editor_state(indoc!(
15737 "fn a() {
15738 // dog();
15739 ˇ
15740 cat();
15741 }"
15742 ));
15743
15744 // Single cursor on one line -> advance
15745 // Cursor starts and ends at column 0
15746 cx.set_state(indoc!(
15747 "fn a() {
15748 ˇ dog();
15749 cat();
15750 }"
15751 ));
15752 cx.update_editor(|editor, window, cx| {
15753 editor.toggle_comments(toggle_comments, window, cx);
15754 });
15755 cx.assert_editor_state(indoc!(
15756 "fn a() {
15757 // dog();
15758 ˇ cat();
15759 }"
15760 ));
15761}
15762
15763#[gpui::test]
15764async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15765 init_test(cx, |_| {});
15766
15767 let mut cx = EditorTestContext::new(cx).await;
15768
15769 let html_language = Arc::new(
15770 Language::new(
15771 LanguageConfig {
15772 name: "HTML".into(),
15773 block_comment: Some(BlockCommentConfig {
15774 start: "<!-- ".into(),
15775 prefix: "".into(),
15776 end: " -->".into(),
15777 tab_size: 0,
15778 }),
15779 ..Default::default()
15780 },
15781 Some(tree_sitter_html::LANGUAGE.into()),
15782 )
15783 .with_injection_query(
15784 r#"
15785 (script_element
15786 (raw_text) @injection.content
15787 (#set! injection.language "javascript"))
15788 "#,
15789 )
15790 .unwrap(),
15791 );
15792
15793 let javascript_language = Arc::new(Language::new(
15794 LanguageConfig {
15795 name: "JavaScript".into(),
15796 line_comments: vec!["// ".into()],
15797 ..Default::default()
15798 },
15799 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15800 ));
15801
15802 cx.language_registry().add(html_language.clone());
15803 cx.language_registry().add(javascript_language);
15804 cx.update_buffer(|buffer, cx| {
15805 buffer.set_language(Some(html_language), cx);
15806 });
15807
15808 // Toggle comments for empty selections
15809 cx.set_state(
15810 &r#"
15811 <p>A</p>ˇ
15812 <p>B</p>ˇ
15813 <p>C</p>ˇ
15814 "#
15815 .unindent(),
15816 );
15817 cx.update_editor(|editor, window, cx| {
15818 editor.toggle_comments(&ToggleComments::default(), window, cx)
15819 });
15820 cx.assert_editor_state(
15821 &r#"
15822 <!-- <p>A</p>ˇ -->
15823 <!-- <p>B</p>ˇ -->
15824 <!-- <p>C</p>ˇ -->
15825 "#
15826 .unindent(),
15827 );
15828 cx.update_editor(|editor, window, cx| {
15829 editor.toggle_comments(&ToggleComments::default(), window, cx)
15830 });
15831 cx.assert_editor_state(
15832 &r#"
15833 <p>A</p>ˇ
15834 <p>B</p>ˇ
15835 <p>C</p>ˇ
15836 "#
15837 .unindent(),
15838 );
15839
15840 // Toggle comments for mixture of empty and non-empty selections, where
15841 // multiple selections occupy a given line.
15842 cx.set_state(
15843 &r#"
15844 <p>A«</p>
15845 <p>ˇ»B</p>ˇ
15846 <p>C«</p>
15847 <p>ˇ»D</p>ˇ
15848 "#
15849 .unindent(),
15850 );
15851
15852 cx.update_editor(|editor, window, cx| {
15853 editor.toggle_comments(&ToggleComments::default(), window, cx)
15854 });
15855 cx.assert_editor_state(
15856 &r#"
15857 <!-- <p>A«</p>
15858 <p>ˇ»B</p>ˇ -->
15859 <!-- <p>C«</p>
15860 <p>ˇ»D</p>ˇ -->
15861 "#
15862 .unindent(),
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
15877 // Toggle comments when different languages are active for different
15878 // selections.
15879 cx.set_state(
15880 &r#"
15881 ˇ<script>
15882 ˇvar x = new Y();
15883 ˇ</script>
15884 "#
15885 .unindent(),
15886 );
15887 cx.executor().run_until_parked();
15888 cx.update_editor(|editor, window, cx| {
15889 editor.toggle_comments(&ToggleComments::default(), window, cx)
15890 });
15891 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15892 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15893 cx.assert_editor_state(
15894 &r#"
15895 <!-- ˇ<script> -->
15896 // ˇvar x = new Y();
15897 <!-- ˇ</script> -->
15898 "#
15899 .unindent(),
15900 );
15901}
15902
15903#[gpui::test]
15904fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15905 init_test(cx, |_| {});
15906
15907 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15908 let multibuffer = cx.new(|cx| {
15909 let mut multibuffer = MultiBuffer::new(ReadWrite);
15910 multibuffer.push_excerpts(
15911 buffer.clone(),
15912 [
15913 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15914 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15915 ],
15916 cx,
15917 );
15918 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15919 multibuffer
15920 });
15921
15922 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15923 editor.update_in(cx, |editor, window, cx| {
15924 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15925 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15926 s.select_ranges([
15927 Point::new(0, 0)..Point::new(0, 0),
15928 Point::new(1, 0)..Point::new(1, 0),
15929 ])
15930 });
15931
15932 editor.handle_input("X", window, cx);
15933 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15934 assert_eq!(
15935 editor.selections.ranges(cx),
15936 [
15937 Point::new(0, 1)..Point::new(0, 1),
15938 Point::new(1, 1)..Point::new(1, 1),
15939 ]
15940 );
15941
15942 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15943 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15944 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15945 });
15946 editor.backspace(&Default::default(), window, cx);
15947 assert_eq!(editor.text(cx), "Xa\nbbb");
15948 assert_eq!(
15949 editor.selections.ranges(cx),
15950 [Point::new(1, 0)..Point::new(1, 0)]
15951 );
15952
15953 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15954 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15955 });
15956 editor.backspace(&Default::default(), window, cx);
15957 assert_eq!(editor.text(cx), "X\nbb");
15958 assert_eq!(
15959 editor.selections.ranges(cx),
15960 [Point::new(0, 1)..Point::new(0, 1)]
15961 );
15962 });
15963}
15964
15965#[gpui::test]
15966fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15967 init_test(cx, |_| {});
15968
15969 let markers = vec![('[', ']').into(), ('(', ')').into()];
15970 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15971 indoc! {"
15972 [aaaa
15973 (bbbb]
15974 cccc)",
15975 },
15976 markers.clone(),
15977 );
15978 let excerpt_ranges = markers.into_iter().map(|marker| {
15979 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15980 ExcerptRange::new(context)
15981 });
15982 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15983 let multibuffer = cx.new(|cx| {
15984 let mut multibuffer = MultiBuffer::new(ReadWrite);
15985 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15986 multibuffer
15987 });
15988
15989 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15990 editor.update_in(cx, |editor, window, cx| {
15991 let (expected_text, selection_ranges) = marked_text_ranges(
15992 indoc! {"
15993 aaaa
15994 bˇbbb
15995 bˇbbˇb
15996 cccc"
15997 },
15998 true,
15999 );
16000 assert_eq!(editor.text(cx), expected_text);
16001 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16002 s.select_ranges(selection_ranges)
16003 });
16004
16005 editor.handle_input("X", window, cx);
16006
16007 let (expected_text, expected_selections) = marked_text_ranges(
16008 indoc! {"
16009 aaaa
16010 bXˇbbXb
16011 bXˇbbXˇb
16012 cccc"
16013 },
16014 false,
16015 );
16016 assert_eq!(editor.text(cx), expected_text);
16017 assert_eq!(editor.selections.ranges(cx), expected_selections);
16018
16019 editor.newline(&Newline, window, cx);
16020 let (expected_text, expected_selections) = marked_text_ranges(
16021 indoc! {"
16022 aaaa
16023 bX
16024 ˇbbX
16025 b
16026 bX
16027 ˇbbX
16028 ˇb
16029 cccc"
16030 },
16031 false,
16032 );
16033 assert_eq!(editor.text(cx), expected_text);
16034 assert_eq!(editor.selections.ranges(cx), expected_selections);
16035 });
16036}
16037
16038#[gpui::test]
16039fn test_refresh_selections(cx: &mut TestAppContext) {
16040 init_test(cx, |_| {});
16041
16042 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16043 let mut excerpt1_id = None;
16044 let multibuffer = cx.new(|cx| {
16045 let mut multibuffer = MultiBuffer::new(ReadWrite);
16046 excerpt1_id = multibuffer
16047 .push_excerpts(
16048 buffer.clone(),
16049 [
16050 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16051 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16052 ],
16053 cx,
16054 )
16055 .into_iter()
16056 .next();
16057 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16058 multibuffer
16059 });
16060
16061 let editor = cx.add_window(|window, cx| {
16062 let mut editor = build_editor(multibuffer.clone(), window, cx);
16063 let snapshot = editor.snapshot(window, cx);
16064 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16065 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16066 });
16067 editor.begin_selection(
16068 Point::new(2, 1).to_display_point(&snapshot),
16069 true,
16070 1,
16071 window,
16072 cx,
16073 );
16074 assert_eq!(
16075 editor.selections.ranges(cx),
16076 [
16077 Point::new(1, 3)..Point::new(1, 3),
16078 Point::new(2, 1)..Point::new(2, 1),
16079 ]
16080 );
16081 editor
16082 });
16083
16084 // Refreshing selections is a no-op when excerpts haven't changed.
16085 _ = editor.update(cx, |editor, window, cx| {
16086 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16087 assert_eq!(
16088 editor.selections.ranges(cx),
16089 [
16090 Point::new(1, 3)..Point::new(1, 3),
16091 Point::new(2, 1)..Point::new(2, 1),
16092 ]
16093 );
16094 });
16095
16096 multibuffer.update(cx, |multibuffer, cx| {
16097 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16098 });
16099 _ = editor.update(cx, |editor, window, cx| {
16100 // Removing an excerpt causes the first selection to become degenerate.
16101 assert_eq!(
16102 editor.selections.ranges(cx),
16103 [
16104 Point::new(0, 0)..Point::new(0, 0),
16105 Point::new(0, 1)..Point::new(0, 1)
16106 ]
16107 );
16108
16109 // Refreshing selections will relocate the first selection to the original buffer
16110 // location.
16111 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16112 assert_eq!(
16113 editor.selections.ranges(cx),
16114 [
16115 Point::new(0, 1)..Point::new(0, 1),
16116 Point::new(0, 3)..Point::new(0, 3)
16117 ]
16118 );
16119 assert!(editor.selections.pending_anchor().is_some());
16120 });
16121}
16122
16123#[gpui::test]
16124fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16125 init_test(cx, |_| {});
16126
16127 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16128 let mut excerpt1_id = None;
16129 let multibuffer = cx.new(|cx| {
16130 let mut multibuffer = MultiBuffer::new(ReadWrite);
16131 excerpt1_id = multibuffer
16132 .push_excerpts(
16133 buffer.clone(),
16134 [
16135 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16136 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16137 ],
16138 cx,
16139 )
16140 .into_iter()
16141 .next();
16142 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16143 multibuffer
16144 });
16145
16146 let editor = cx.add_window(|window, cx| {
16147 let mut editor = build_editor(multibuffer.clone(), window, cx);
16148 let snapshot = editor.snapshot(window, cx);
16149 editor.begin_selection(
16150 Point::new(1, 3).to_display_point(&snapshot),
16151 false,
16152 1,
16153 window,
16154 cx,
16155 );
16156 assert_eq!(
16157 editor.selections.ranges(cx),
16158 [Point::new(1, 3)..Point::new(1, 3)]
16159 );
16160 editor
16161 });
16162
16163 multibuffer.update(cx, |multibuffer, cx| {
16164 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16165 });
16166 _ = editor.update(cx, |editor, window, cx| {
16167 assert_eq!(
16168 editor.selections.ranges(cx),
16169 [Point::new(0, 0)..Point::new(0, 0)]
16170 );
16171
16172 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16173 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16174 assert_eq!(
16175 editor.selections.ranges(cx),
16176 [Point::new(0, 3)..Point::new(0, 3)]
16177 );
16178 assert!(editor.selections.pending_anchor().is_some());
16179 });
16180}
16181
16182#[gpui::test]
16183async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16184 init_test(cx, |_| {});
16185
16186 let language = Arc::new(
16187 Language::new(
16188 LanguageConfig {
16189 brackets: BracketPairConfig {
16190 pairs: vec![
16191 BracketPair {
16192 start: "{".to_string(),
16193 end: "}".to_string(),
16194 close: true,
16195 surround: true,
16196 newline: true,
16197 },
16198 BracketPair {
16199 start: "/* ".to_string(),
16200 end: " */".to_string(),
16201 close: true,
16202 surround: true,
16203 newline: true,
16204 },
16205 ],
16206 ..Default::default()
16207 },
16208 ..Default::default()
16209 },
16210 Some(tree_sitter_rust::LANGUAGE.into()),
16211 )
16212 .with_indents_query("")
16213 .unwrap(),
16214 );
16215
16216 let text = concat!(
16217 "{ }\n", //
16218 " x\n", //
16219 " /* */\n", //
16220 "x\n", //
16221 "{{} }\n", //
16222 );
16223
16224 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16225 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16226 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16227 editor
16228 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16229 .await;
16230
16231 editor.update_in(cx, |editor, window, cx| {
16232 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16233 s.select_display_ranges([
16234 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16235 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16236 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16237 ])
16238 });
16239 editor.newline(&Newline, window, cx);
16240
16241 assert_eq!(
16242 editor.buffer().read(cx).read(cx).text(),
16243 concat!(
16244 "{ \n", // Suppress rustfmt
16245 "\n", //
16246 "}\n", //
16247 " x\n", //
16248 " /* \n", //
16249 " \n", //
16250 " */\n", //
16251 "x\n", //
16252 "{{} \n", //
16253 "}\n", //
16254 )
16255 );
16256 });
16257}
16258
16259#[gpui::test]
16260fn test_highlighted_ranges(cx: &mut TestAppContext) {
16261 init_test(cx, |_| {});
16262
16263 let editor = cx.add_window(|window, cx| {
16264 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16265 build_editor(buffer, window, cx)
16266 });
16267
16268 _ = editor.update(cx, |editor, window, cx| {
16269 struct Type1;
16270 struct Type2;
16271
16272 let buffer = editor.buffer.read(cx).snapshot(cx);
16273
16274 let anchor_range =
16275 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16276
16277 editor.highlight_background::<Type1>(
16278 &[
16279 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16280 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16281 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16282 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16283 ],
16284 |_| Hsla::red(),
16285 cx,
16286 );
16287 editor.highlight_background::<Type2>(
16288 &[
16289 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16290 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16291 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16292 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16293 ],
16294 |_| Hsla::green(),
16295 cx,
16296 );
16297
16298 let snapshot = editor.snapshot(window, cx);
16299 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16300 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16301 &snapshot,
16302 cx.theme(),
16303 );
16304 assert_eq!(
16305 highlighted_ranges,
16306 &[
16307 (
16308 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16309 Hsla::green(),
16310 ),
16311 (
16312 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16313 Hsla::red(),
16314 ),
16315 (
16316 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16317 Hsla::green(),
16318 ),
16319 (
16320 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16321 Hsla::red(),
16322 ),
16323 ]
16324 );
16325 assert_eq!(
16326 editor.sorted_background_highlights_in_range(
16327 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16328 &snapshot,
16329 cx.theme(),
16330 ),
16331 &[(
16332 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16333 Hsla::red(),
16334 )]
16335 );
16336 });
16337}
16338
16339#[gpui::test]
16340async fn test_following(cx: &mut TestAppContext) {
16341 init_test(cx, |_| {});
16342
16343 let fs = FakeFs::new(cx.executor());
16344 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16345
16346 let buffer = project.update(cx, |project, cx| {
16347 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16348 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16349 });
16350 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16351 let follower = cx.update(|cx| {
16352 cx.open_window(
16353 WindowOptions {
16354 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16355 gpui::Point::new(px(0.), px(0.)),
16356 gpui::Point::new(px(10.), px(80.)),
16357 ))),
16358 ..Default::default()
16359 },
16360 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16361 )
16362 .unwrap()
16363 });
16364
16365 let is_still_following = Rc::new(RefCell::new(true));
16366 let follower_edit_event_count = Rc::new(RefCell::new(0));
16367 let pending_update = Rc::new(RefCell::new(None));
16368 let leader_entity = leader.root(cx).unwrap();
16369 let follower_entity = follower.root(cx).unwrap();
16370 _ = follower.update(cx, {
16371 let update = pending_update.clone();
16372 let is_still_following = is_still_following.clone();
16373 let follower_edit_event_count = follower_edit_event_count.clone();
16374 |_, window, cx| {
16375 cx.subscribe_in(
16376 &leader_entity,
16377 window,
16378 move |_, leader, event, window, cx| {
16379 leader.read(cx).add_event_to_update_proto(
16380 event,
16381 &mut update.borrow_mut(),
16382 window,
16383 cx,
16384 );
16385 },
16386 )
16387 .detach();
16388
16389 cx.subscribe_in(
16390 &follower_entity,
16391 window,
16392 move |_, _, event: &EditorEvent, _window, _cx| {
16393 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16394 *is_still_following.borrow_mut() = false;
16395 }
16396
16397 if let EditorEvent::BufferEdited = event {
16398 *follower_edit_event_count.borrow_mut() += 1;
16399 }
16400 },
16401 )
16402 .detach();
16403 }
16404 });
16405
16406 // Update the selections only
16407 _ = leader.update(cx, |leader, window, cx| {
16408 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16409 s.select_ranges([1..1])
16410 });
16411 });
16412 follower
16413 .update(cx, |follower, window, cx| {
16414 follower.apply_update_proto(
16415 &project,
16416 pending_update.borrow_mut().take().unwrap(),
16417 window,
16418 cx,
16419 )
16420 })
16421 .unwrap()
16422 .await
16423 .unwrap();
16424 _ = follower.update(cx, |follower, _, cx| {
16425 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
16426 });
16427 assert!(*is_still_following.borrow());
16428 assert_eq!(*follower_edit_event_count.borrow(), 0);
16429
16430 // Update the scroll position only
16431 _ = leader.update(cx, |leader, window, cx| {
16432 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16433 });
16434 follower
16435 .update(cx, |follower, window, cx| {
16436 follower.apply_update_proto(
16437 &project,
16438 pending_update.borrow_mut().take().unwrap(),
16439 window,
16440 cx,
16441 )
16442 })
16443 .unwrap()
16444 .await
16445 .unwrap();
16446 assert_eq!(
16447 follower
16448 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16449 .unwrap(),
16450 gpui::Point::new(1.5, 3.5)
16451 );
16452 assert!(*is_still_following.borrow());
16453 assert_eq!(*follower_edit_event_count.borrow(), 0);
16454
16455 // Update the selections and scroll position. The follower's scroll position is updated
16456 // via autoscroll, not via the leader's exact scroll position.
16457 _ = leader.update(cx, |leader, window, cx| {
16458 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16459 s.select_ranges([0..0])
16460 });
16461 leader.request_autoscroll(Autoscroll::newest(), cx);
16462 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16463 });
16464 follower
16465 .update(cx, |follower, window, cx| {
16466 follower.apply_update_proto(
16467 &project,
16468 pending_update.borrow_mut().take().unwrap(),
16469 window,
16470 cx,
16471 )
16472 })
16473 .unwrap()
16474 .await
16475 .unwrap();
16476 _ = follower.update(cx, |follower, _, cx| {
16477 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16478 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16479 });
16480 assert!(*is_still_following.borrow());
16481
16482 // Creating a pending selection that precedes another selection
16483 _ = leader.update(cx, |leader, window, cx| {
16484 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16485 s.select_ranges([1..1])
16486 });
16487 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16488 });
16489 follower
16490 .update(cx, |follower, window, cx| {
16491 follower.apply_update_proto(
16492 &project,
16493 pending_update.borrow_mut().take().unwrap(),
16494 window,
16495 cx,
16496 )
16497 })
16498 .unwrap()
16499 .await
16500 .unwrap();
16501 _ = follower.update(cx, |follower, _, cx| {
16502 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16503 });
16504 assert!(*is_still_following.borrow());
16505
16506 // Extend the pending selection so that it surrounds another selection
16507 _ = leader.update(cx, |leader, window, cx| {
16508 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16509 });
16510 follower
16511 .update(cx, |follower, window, cx| {
16512 follower.apply_update_proto(
16513 &project,
16514 pending_update.borrow_mut().take().unwrap(),
16515 window,
16516 cx,
16517 )
16518 })
16519 .unwrap()
16520 .await
16521 .unwrap();
16522 _ = follower.update(cx, |follower, _, cx| {
16523 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16524 });
16525
16526 // Scrolling locally breaks the follow
16527 _ = follower.update(cx, |follower, window, cx| {
16528 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16529 follower.set_scroll_anchor(
16530 ScrollAnchor {
16531 anchor: top_anchor,
16532 offset: gpui::Point::new(0.0, 0.5),
16533 },
16534 window,
16535 cx,
16536 );
16537 });
16538 assert!(!(*is_still_following.borrow()));
16539}
16540
16541#[gpui::test]
16542async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16543 init_test(cx, |_| {});
16544
16545 let fs = FakeFs::new(cx.executor());
16546 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16547 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16548 let pane = workspace
16549 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16550 .unwrap();
16551
16552 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16553
16554 let leader = pane.update_in(cx, |_, window, cx| {
16555 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16556 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16557 });
16558
16559 // Start following the editor when it has no excerpts.
16560 let mut state_message =
16561 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16562 let workspace_entity = workspace.root(cx).unwrap();
16563 let follower_1 = cx
16564 .update_window(*workspace.deref(), |_, window, cx| {
16565 Editor::from_state_proto(
16566 workspace_entity,
16567 ViewId {
16568 creator: CollaboratorId::PeerId(PeerId::default()),
16569 id: 0,
16570 },
16571 &mut state_message,
16572 window,
16573 cx,
16574 )
16575 })
16576 .unwrap()
16577 .unwrap()
16578 .await
16579 .unwrap();
16580
16581 let update_message = Rc::new(RefCell::new(None));
16582 follower_1.update_in(cx, {
16583 let update = update_message.clone();
16584 |_, window, cx| {
16585 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16586 leader.read(cx).add_event_to_update_proto(
16587 event,
16588 &mut update.borrow_mut(),
16589 window,
16590 cx,
16591 );
16592 })
16593 .detach();
16594 }
16595 });
16596
16597 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16598 (
16599 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16600 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16601 )
16602 });
16603
16604 // Insert some excerpts.
16605 leader.update(cx, |leader, cx| {
16606 leader.buffer.update(cx, |multibuffer, cx| {
16607 multibuffer.set_excerpts_for_path(
16608 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
16609 buffer_1.clone(),
16610 vec![
16611 Point::row_range(0..3),
16612 Point::row_range(1..6),
16613 Point::row_range(12..15),
16614 ],
16615 0,
16616 cx,
16617 );
16618 multibuffer.set_excerpts_for_path(
16619 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
16620 buffer_2.clone(),
16621 vec![Point::row_range(0..6), Point::row_range(8..12)],
16622 0,
16623 cx,
16624 );
16625 });
16626 });
16627
16628 // Apply the update of adding the excerpts.
16629 follower_1
16630 .update_in(cx, |follower, window, cx| {
16631 follower.apply_update_proto(
16632 &project,
16633 update_message.borrow().clone().unwrap(),
16634 window,
16635 cx,
16636 )
16637 })
16638 .await
16639 .unwrap();
16640 assert_eq!(
16641 follower_1.update(cx, |editor, cx| editor.text(cx)),
16642 leader.update(cx, |editor, cx| editor.text(cx))
16643 );
16644 update_message.borrow_mut().take();
16645
16646 // Start following separately after it already has excerpts.
16647 let mut state_message =
16648 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16649 let workspace_entity = workspace.root(cx).unwrap();
16650 let follower_2 = cx
16651 .update_window(*workspace.deref(), |_, window, cx| {
16652 Editor::from_state_proto(
16653 workspace_entity,
16654 ViewId {
16655 creator: CollaboratorId::PeerId(PeerId::default()),
16656 id: 0,
16657 },
16658 &mut state_message,
16659 window,
16660 cx,
16661 )
16662 })
16663 .unwrap()
16664 .unwrap()
16665 .await
16666 .unwrap();
16667 assert_eq!(
16668 follower_2.update(cx, |editor, cx| editor.text(cx)),
16669 leader.update(cx, |editor, cx| editor.text(cx))
16670 );
16671
16672 // Remove some excerpts.
16673 leader.update(cx, |leader, cx| {
16674 leader.buffer.update(cx, |multibuffer, cx| {
16675 let excerpt_ids = multibuffer.excerpt_ids();
16676 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16677 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16678 });
16679 });
16680
16681 // Apply the update of removing the excerpts.
16682 follower_1
16683 .update_in(cx, |follower, window, cx| {
16684 follower.apply_update_proto(
16685 &project,
16686 update_message.borrow().clone().unwrap(),
16687 window,
16688 cx,
16689 )
16690 })
16691 .await
16692 .unwrap();
16693 follower_2
16694 .update_in(cx, |follower, window, cx| {
16695 follower.apply_update_proto(
16696 &project,
16697 update_message.borrow().clone().unwrap(),
16698 window,
16699 cx,
16700 )
16701 })
16702 .await
16703 .unwrap();
16704 update_message.borrow_mut().take();
16705 assert_eq!(
16706 follower_1.update(cx, |editor, cx| editor.text(cx)),
16707 leader.update(cx, |editor, cx| editor.text(cx))
16708 );
16709}
16710
16711#[gpui::test]
16712async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16713 init_test(cx, |_| {});
16714
16715 let mut cx = EditorTestContext::new(cx).await;
16716 let lsp_store =
16717 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16718
16719 cx.set_state(indoc! {"
16720 ˇfn func(abc def: i32) -> u32 {
16721 }
16722 "});
16723
16724 cx.update(|_, cx| {
16725 lsp_store.update(cx, |lsp_store, cx| {
16726 lsp_store
16727 .update_diagnostics(
16728 LanguageServerId(0),
16729 lsp::PublishDiagnosticsParams {
16730 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16731 version: None,
16732 diagnostics: vec![
16733 lsp::Diagnostic {
16734 range: lsp::Range::new(
16735 lsp::Position::new(0, 11),
16736 lsp::Position::new(0, 12),
16737 ),
16738 severity: Some(lsp::DiagnosticSeverity::ERROR),
16739 ..Default::default()
16740 },
16741 lsp::Diagnostic {
16742 range: lsp::Range::new(
16743 lsp::Position::new(0, 12),
16744 lsp::Position::new(0, 15),
16745 ),
16746 severity: Some(lsp::DiagnosticSeverity::ERROR),
16747 ..Default::default()
16748 },
16749 lsp::Diagnostic {
16750 range: lsp::Range::new(
16751 lsp::Position::new(0, 25),
16752 lsp::Position::new(0, 28),
16753 ),
16754 severity: Some(lsp::DiagnosticSeverity::ERROR),
16755 ..Default::default()
16756 },
16757 ],
16758 },
16759 None,
16760 DiagnosticSourceKind::Pushed,
16761 &[],
16762 cx,
16763 )
16764 .unwrap()
16765 });
16766 });
16767
16768 executor.run_until_parked();
16769
16770 cx.update_editor(|editor, window, cx| {
16771 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16772 });
16773
16774 cx.assert_editor_state(indoc! {"
16775 fn func(abc def: i32) -> ˇu32 {
16776 }
16777 "});
16778
16779 cx.update_editor(|editor, window, cx| {
16780 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16781 });
16782
16783 cx.assert_editor_state(indoc! {"
16784 fn func(abc ˇdef: i32) -> u32 {
16785 }
16786 "});
16787
16788 cx.update_editor(|editor, window, cx| {
16789 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16790 });
16791
16792 cx.assert_editor_state(indoc! {"
16793 fn func(abcˇ def: i32) -> u32 {
16794 }
16795 "});
16796
16797 cx.update_editor(|editor, window, cx| {
16798 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16799 });
16800
16801 cx.assert_editor_state(indoc! {"
16802 fn func(abc def: i32) -> ˇu32 {
16803 }
16804 "});
16805}
16806
16807#[gpui::test]
16808async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16809 init_test(cx, |_| {});
16810
16811 let mut cx = EditorTestContext::new(cx).await;
16812
16813 let diff_base = r#"
16814 use some::mod;
16815
16816 const A: u32 = 42;
16817
16818 fn main() {
16819 println!("hello");
16820
16821 println!("world");
16822 }
16823 "#
16824 .unindent();
16825
16826 // Edits are modified, removed, modified, added
16827 cx.set_state(
16828 &r#"
16829 use some::modified;
16830
16831 ˇ
16832 fn main() {
16833 println!("hello there");
16834
16835 println!("around the");
16836 println!("world");
16837 }
16838 "#
16839 .unindent(),
16840 );
16841
16842 cx.set_head_text(&diff_base);
16843 executor.run_until_parked();
16844
16845 cx.update_editor(|editor, window, cx| {
16846 //Wrap around the bottom of the buffer
16847 for _ in 0..3 {
16848 editor.go_to_next_hunk(&GoToHunk, window, cx);
16849 }
16850 });
16851
16852 cx.assert_editor_state(
16853 &r#"
16854 ˇuse some::modified;
16855
16856
16857 fn main() {
16858 println!("hello there");
16859
16860 println!("around the");
16861 println!("world");
16862 }
16863 "#
16864 .unindent(),
16865 );
16866
16867 cx.update_editor(|editor, window, cx| {
16868 //Wrap around the top of the buffer
16869 for _ in 0..2 {
16870 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16871 }
16872 });
16873
16874 cx.assert_editor_state(
16875 &r#"
16876 use some::modified;
16877
16878
16879 fn main() {
16880 ˇ println!("hello there");
16881
16882 println!("around the");
16883 println!("world");
16884 }
16885 "#
16886 .unindent(),
16887 );
16888
16889 cx.update_editor(|editor, window, cx| {
16890 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16891 });
16892
16893 cx.assert_editor_state(
16894 &r#"
16895 use some::modified;
16896
16897 ˇ
16898 fn main() {
16899 println!("hello there");
16900
16901 println!("around the");
16902 println!("world");
16903 }
16904 "#
16905 .unindent(),
16906 );
16907
16908 cx.update_editor(|editor, window, cx| {
16909 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16910 });
16911
16912 cx.assert_editor_state(
16913 &r#"
16914 ˇuse some::modified;
16915
16916
16917 fn main() {
16918 println!("hello there");
16919
16920 println!("around the");
16921 println!("world");
16922 }
16923 "#
16924 .unindent(),
16925 );
16926
16927 cx.update_editor(|editor, window, cx| {
16928 for _ in 0..2 {
16929 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16930 }
16931 });
16932
16933 cx.assert_editor_state(
16934 &r#"
16935 use some::modified;
16936
16937
16938 fn main() {
16939 ˇ println!("hello there");
16940
16941 println!("around the");
16942 println!("world");
16943 }
16944 "#
16945 .unindent(),
16946 );
16947
16948 cx.update_editor(|editor, window, cx| {
16949 editor.fold(&Fold, window, cx);
16950 });
16951
16952 cx.update_editor(|editor, window, cx| {
16953 editor.go_to_next_hunk(&GoToHunk, window, cx);
16954 });
16955
16956 cx.assert_editor_state(
16957 &r#"
16958 ˇuse some::modified;
16959
16960
16961 fn main() {
16962 println!("hello there");
16963
16964 println!("around the");
16965 println!("world");
16966 }
16967 "#
16968 .unindent(),
16969 );
16970}
16971
16972#[test]
16973fn test_split_words() {
16974 fn split(text: &str) -> Vec<&str> {
16975 split_words(text).collect()
16976 }
16977
16978 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16979 assert_eq!(split("hello_world"), &["hello_", "world"]);
16980 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16981 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16982 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16983 assert_eq!(split("helloworld"), &["helloworld"]);
16984
16985 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16986}
16987
16988#[gpui::test]
16989async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16990 init_test(cx, |_| {});
16991
16992 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16993 let mut assert = |before, after| {
16994 let _state_context = cx.set_state(before);
16995 cx.run_until_parked();
16996 cx.update_editor(|editor, window, cx| {
16997 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16998 });
16999 cx.run_until_parked();
17000 cx.assert_editor_state(after);
17001 };
17002
17003 // Outside bracket jumps to outside of matching bracket
17004 assert("console.logˇ(var);", "console.log(var)ˇ;");
17005 assert("console.log(var)ˇ;", "console.logˇ(var);");
17006
17007 // Inside bracket jumps to inside of matching bracket
17008 assert("console.log(ˇvar);", "console.log(varˇ);");
17009 assert("console.log(varˇ);", "console.log(ˇvar);");
17010
17011 // When outside a bracket and inside, favor jumping to the inside bracket
17012 assert(
17013 "console.log('foo', [1, 2, 3]ˇ);",
17014 "console.log(ˇ'foo', [1, 2, 3]);",
17015 );
17016 assert(
17017 "console.log(ˇ'foo', [1, 2, 3]);",
17018 "console.log('foo', [1, 2, 3]ˇ);",
17019 );
17020
17021 // Bias forward if two options are equally likely
17022 assert(
17023 "let result = curried_fun()ˇ();",
17024 "let result = curried_fun()()ˇ;",
17025 );
17026
17027 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17028 assert(
17029 indoc! {"
17030 function test() {
17031 console.log('test')ˇ
17032 }"},
17033 indoc! {"
17034 function test() {
17035 console.logˇ('test')
17036 }"},
17037 );
17038}
17039
17040#[gpui::test]
17041async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17042 init_test(cx, |_| {});
17043
17044 let fs = FakeFs::new(cx.executor());
17045 fs.insert_tree(
17046 path!("/a"),
17047 json!({
17048 "main.rs": "fn main() { let a = 5; }",
17049 "other.rs": "// Test file",
17050 }),
17051 )
17052 .await;
17053 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17054
17055 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17056 language_registry.add(Arc::new(Language::new(
17057 LanguageConfig {
17058 name: "Rust".into(),
17059 matcher: LanguageMatcher {
17060 path_suffixes: vec!["rs".to_string()],
17061 ..Default::default()
17062 },
17063 brackets: BracketPairConfig {
17064 pairs: vec![BracketPair {
17065 start: "{".to_string(),
17066 end: "}".to_string(),
17067 close: true,
17068 surround: true,
17069 newline: true,
17070 }],
17071 disabled_scopes_by_bracket_ix: Vec::new(),
17072 },
17073 ..Default::default()
17074 },
17075 Some(tree_sitter_rust::LANGUAGE.into()),
17076 )));
17077 let mut fake_servers = language_registry.register_fake_lsp(
17078 "Rust",
17079 FakeLspAdapter {
17080 capabilities: lsp::ServerCapabilities {
17081 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17082 first_trigger_character: "{".to_string(),
17083 more_trigger_character: None,
17084 }),
17085 ..Default::default()
17086 },
17087 ..Default::default()
17088 },
17089 );
17090
17091 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17092
17093 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17094
17095 let worktree_id = workspace
17096 .update(cx, |workspace, _, cx| {
17097 workspace.project().update(cx, |project, cx| {
17098 project.worktrees(cx).next().unwrap().read(cx).id()
17099 })
17100 })
17101 .unwrap();
17102
17103 let buffer = project
17104 .update(cx, |project, cx| {
17105 project.open_local_buffer(path!("/a/main.rs"), cx)
17106 })
17107 .await
17108 .unwrap();
17109 let editor_handle = workspace
17110 .update(cx, |workspace, window, cx| {
17111 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17112 })
17113 .unwrap()
17114 .await
17115 .unwrap()
17116 .downcast::<Editor>()
17117 .unwrap();
17118
17119 cx.executor().start_waiting();
17120 let fake_server = fake_servers.next().await.unwrap();
17121
17122 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17123 |params, _| async move {
17124 assert_eq!(
17125 params.text_document_position.text_document.uri,
17126 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17127 );
17128 assert_eq!(
17129 params.text_document_position.position,
17130 lsp::Position::new(0, 21),
17131 );
17132
17133 Ok(Some(vec![lsp::TextEdit {
17134 new_text: "]".to_string(),
17135 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17136 }]))
17137 },
17138 );
17139
17140 editor_handle.update_in(cx, |editor, window, cx| {
17141 window.focus(&editor.focus_handle(cx));
17142 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17143 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17144 });
17145 editor.handle_input("{", window, cx);
17146 });
17147
17148 cx.executor().run_until_parked();
17149
17150 buffer.update(cx, |buffer, _| {
17151 assert_eq!(
17152 buffer.text(),
17153 "fn main() { let a = {5}; }",
17154 "No extra braces from on type formatting should appear in the buffer"
17155 )
17156 });
17157}
17158
17159#[gpui::test(iterations = 20, seeds(31))]
17160async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17161 init_test(cx, |_| {});
17162
17163 let mut cx = EditorLspTestContext::new_rust(
17164 lsp::ServerCapabilities {
17165 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17166 first_trigger_character: ".".to_string(),
17167 more_trigger_character: None,
17168 }),
17169 ..Default::default()
17170 },
17171 cx,
17172 )
17173 .await;
17174
17175 cx.update_buffer(|buffer, _| {
17176 // This causes autoindent to be async.
17177 buffer.set_sync_parse_timeout(Duration::ZERO)
17178 });
17179
17180 cx.set_state("fn c() {\n d()ˇ\n}\n");
17181 cx.simulate_keystroke("\n");
17182 cx.run_until_parked();
17183
17184 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17185 let mut request =
17186 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17187 let buffer_cloned = buffer_cloned.clone();
17188 async move {
17189 buffer_cloned.update(&mut cx, |buffer, _| {
17190 assert_eq!(
17191 buffer.text(),
17192 "fn c() {\n d()\n .\n}\n",
17193 "OnTypeFormatting should triggered after autoindent applied"
17194 )
17195 })?;
17196
17197 Ok(Some(vec![]))
17198 }
17199 });
17200
17201 cx.simulate_keystroke(".");
17202 cx.run_until_parked();
17203
17204 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17205 assert!(request.next().await.is_some());
17206 request.close();
17207 assert!(request.next().await.is_none());
17208}
17209
17210#[gpui::test]
17211async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17212 init_test(cx, |_| {});
17213
17214 let fs = FakeFs::new(cx.executor());
17215 fs.insert_tree(
17216 path!("/a"),
17217 json!({
17218 "main.rs": "fn main() { let a = 5; }",
17219 "other.rs": "// Test file",
17220 }),
17221 )
17222 .await;
17223
17224 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17225
17226 let server_restarts = Arc::new(AtomicUsize::new(0));
17227 let closure_restarts = Arc::clone(&server_restarts);
17228 let language_server_name = "test language server";
17229 let language_name: LanguageName = "Rust".into();
17230
17231 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17232 language_registry.add(Arc::new(Language::new(
17233 LanguageConfig {
17234 name: language_name.clone(),
17235 matcher: LanguageMatcher {
17236 path_suffixes: vec!["rs".to_string()],
17237 ..Default::default()
17238 },
17239 ..Default::default()
17240 },
17241 Some(tree_sitter_rust::LANGUAGE.into()),
17242 )));
17243 let mut fake_servers = language_registry.register_fake_lsp(
17244 "Rust",
17245 FakeLspAdapter {
17246 name: language_server_name,
17247 initialization_options: Some(json!({
17248 "testOptionValue": true
17249 })),
17250 initializer: Some(Box::new(move |fake_server| {
17251 let task_restarts = Arc::clone(&closure_restarts);
17252 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17253 task_restarts.fetch_add(1, atomic::Ordering::Release);
17254 futures::future::ready(Ok(()))
17255 });
17256 })),
17257 ..Default::default()
17258 },
17259 );
17260
17261 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17262 let _buffer = project
17263 .update(cx, |project, cx| {
17264 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17265 })
17266 .await
17267 .unwrap();
17268 let _fake_server = fake_servers.next().await.unwrap();
17269 update_test_language_settings(cx, |language_settings| {
17270 language_settings.languages.0.insert(
17271 language_name.clone().0,
17272 LanguageSettingsContent {
17273 tab_size: NonZeroU32::new(8),
17274 ..Default::default()
17275 },
17276 );
17277 });
17278 cx.executor().run_until_parked();
17279 assert_eq!(
17280 server_restarts.load(atomic::Ordering::Acquire),
17281 0,
17282 "Should not restart LSP server on an unrelated change"
17283 );
17284
17285 update_test_project_settings(cx, |project_settings| {
17286 project_settings.lsp.insert(
17287 "Some other server name".into(),
17288 LspSettings {
17289 binary: None,
17290 settings: None,
17291 initialization_options: Some(json!({
17292 "some other init value": false
17293 })),
17294 enable_lsp_tasks: false,
17295 fetch: None,
17296 },
17297 );
17298 });
17299 cx.executor().run_until_parked();
17300 assert_eq!(
17301 server_restarts.load(atomic::Ordering::Acquire),
17302 0,
17303 "Should not restart LSP server on an unrelated LSP settings change"
17304 );
17305
17306 update_test_project_settings(cx, |project_settings| {
17307 project_settings.lsp.insert(
17308 language_server_name.into(),
17309 LspSettings {
17310 binary: None,
17311 settings: None,
17312 initialization_options: Some(json!({
17313 "anotherInitValue": false
17314 })),
17315 enable_lsp_tasks: false,
17316 fetch: None,
17317 },
17318 );
17319 });
17320 cx.executor().run_until_parked();
17321 assert_eq!(
17322 server_restarts.load(atomic::Ordering::Acquire),
17323 1,
17324 "Should restart LSP server on a related LSP settings change"
17325 );
17326
17327 update_test_project_settings(cx, |project_settings| {
17328 project_settings.lsp.insert(
17329 language_server_name.into(),
17330 LspSettings {
17331 binary: None,
17332 settings: None,
17333 initialization_options: Some(json!({
17334 "anotherInitValue": false
17335 })),
17336 enable_lsp_tasks: false,
17337 fetch: None,
17338 },
17339 );
17340 });
17341 cx.executor().run_until_parked();
17342 assert_eq!(
17343 server_restarts.load(atomic::Ordering::Acquire),
17344 1,
17345 "Should not restart LSP server on a related LSP settings change that is the same"
17346 );
17347
17348 update_test_project_settings(cx, |project_settings| {
17349 project_settings.lsp.insert(
17350 language_server_name.into(),
17351 LspSettings {
17352 binary: None,
17353 settings: None,
17354 initialization_options: None,
17355 enable_lsp_tasks: false,
17356 fetch: None,
17357 },
17358 );
17359 });
17360 cx.executor().run_until_parked();
17361 assert_eq!(
17362 server_restarts.load(atomic::Ordering::Acquire),
17363 2,
17364 "Should restart LSP server on another related LSP settings change"
17365 );
17366}
17367
17368#[gpui::test]
17369async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17370 init_test(cx, |_| {});
17371
17372 let mut cx = EditorLspTestContext::new_rust(
17373 lsp::ServerCapabilities {
17374 completion_provider: Some(lsp::CompletionOptions {
17375 trigger_characters: Some(vec![".".to_string()]),
17376 resolve_provider: Some(true),
17377 ..Default::default()
17378 }),
17379 ..Default::default()
17380 },
17381 cx,
17382 )
17383 .await;
17384
17385 cx.set_state("fn main() { let a = 2ˇ; }");
17386 cx.simulate_keystroke(".");
17387 let completion_item = lsp::CompletionItem {
17388 label: "some".into(),
17389 kind: Some(lsp::CompletionItemKind::SNIPPET),
17390 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17391 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17392 kind: lsp::MarkupKind::Markdown,
17393 value: "```rust\nSome(2)\n```".to_string(),
17394 })),
17395 deprecated: Some(false),
17396 sort_text: Some("fffffff2".to_string()),
17397 filter_text: Some("some".to_string()),
17398 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17399 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17400 range: lsp::Range {
17401 start: lsp::Position {
17402 line: 0,
17403 character: 22,
17404 },
17405 end: lsp::Position {
17406 line: 0,
17407 character: 22,
17408 },
17409 },
17410 new_text: "Some(2)".to_string(),
17411 })),
17412 additional_text_edits: Some(vec![lsp::TextEdit {
17413 range: lsp::Range {
17414 start: lsp::Position {
17415 line: 0,
17416 character: 20,
17417 },
17418 end: lsp::Position {
17419 line: 0,
17420 character: 22,
17421 },
17422 },
17423 new_text: "".to_string(),
17424 }]),
17425 ..Default::default()
17426 };
17427
17428 let closure_completion_item = completion_item.clone();
17429 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17430 let task_completion_item = closure_completion_item.clone();
17431 async move {
17432 Ok(Some(lsp::CompletionResponse::Array(vec![
17433 task_completion_item,
17434 ])))
17435 }
17436 });
17437
17438 request.next().await;
17439
17440 cx.condition(|editor, _| editor.context_menu_visible())
17441 .await;
17442 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17443 editor
17444 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17445 .unwrap()
17446 });
17447 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17448
17449 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17450 let task_completion_item = completion_item.clone();
17451 async move { Ok(task_completion_item) }
17452 })
17453 .next()
17454 .await
17455 .unwrap();
17456 apply_additional_edits.await.unwrap();
17457 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17458}
17459
17460#[gpui::test]
17461async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17462 init_test(cx, |_| {});
17463
17464 let mut cx = EditorLspTestContext::new_rust(
17465 lsp::ServerCapabilities {
17466 completion_provider: Some(lsp::CompletionOptions {
17467 trigger_characters: Some(vec![".".to_string()]),
17468 resolve_provider: Some(true),
17469 ..Default::default()
17470 }),
17471 ..Default::default()
17472 },
17473 cx,
17474 )
17475 .await;
17476
17477 cx.set_state("fn main() { let a = 2ˇ; }");
17478 cx.simulate_keystroke(".");
17479
17480 let item1 = lsp::CompletionItem {
17481 label: "method id()".to_string(),
17482 filter_text: Some("id".to_string()),
17483 detail: None,
17484 documentation: None,
17485 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17486 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17487 new_text: ".id".to_string(),
17488 })),
17489 ..lsp::CompletionItem::default()
17490 };
17491
17492 let item2 = lsp::CompletionItem {
17493 label: "other".to_string(),
17494 filter_text: Some("other".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: ".other".to_string(),
17500 })),
17501 ..lsp::CompletionItem::default()
17502 };
17503
17504 let item1 = item1.clone();
17505 cx.set_request_handler::<lsp::request::Completion, _, _>({
17506 let item1 = item1.clone();
17507 move |_, _, _| {
17508 let item1 = item1.clone();
17509 let item2 = item2.clone();
17510 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17511 }
17512 })
17513 .next()
17514 .await;
17515
17516 cx.condition(|editor, _| editor.context_menu_visible())
17517 .await;
17518 cx.update_editor(|editor, _, _| {
17519 let context_menu = editor.context_menu.borrow_mut();
17520 let context_menu = context_menu
17521 .as_ref()
17522 .expect("Should have the context menu deployed");
17523 match context_menu {
17524 CodeContextMenu::Completions(completions_menu) => {
17525 let completions = completions_menu.completions.borrow_mut();
17526 assert_eq!(
17527 completions
17528 .iter()
17529 .map(|completion| &completion.label.text)
17530 .collect::<Vec<_>>(),
17531 vec!["method id()", "other"]
17532 )
17533 }
17534 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17535 }
17536 });
17537
17538 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17539 let item1 = item1.clone();
17540 move |_, item_to_resolve, _| {
17541 let item1 = item1.clone();
17542 async move {
17543 if item1 == item_to_resolve {
17544 Ok(lsp::CompletionItem {
17545 label: "method id()".to_string(),
17546 filter_text: Some("id".to_string()),
17547 detail: Some("Now resolved!".to_string()),
17548 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17549 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17550 range: lsp::Range::new(
17551 lsp::Position::new(0, 22),
17552 lsp::Position::new(0, 22),
17553 ),
17554 new_text: ".id".to_string(),
17555 })),
17556 ..lsp::CompletionItem::default()
17557 })
17558 } else {
17559 Ok(item_to_resolve)
17560 }
17561 }
17562 }
17563 })
17564 .next()
17565 .await
17566 .unwrap();
17567 cx.run_until_parked();
17568
17569 cx.update_editor(|editor, window, cx| {
17570 editor.context_menu_next(&Default::default(), window, cx);
17571 });
17572
17573 cx.update_editor(|editor, _, _| {
17574 let context_menu = editor.context_menu.borrow_mut();
17575 let context_menu = context_menu
17576 .as_ref()
17577 .expect("Should have the context menu deployed");
17578 match context_menu {
17579 CodeContextMenu::Completions(completions_menu) => {
17580 let completions = completions_menu.completions.borrow_mut();
17581 assert_eq!(
17582 completions
17583 .iter()
17584 .map(|completion| &completion.label.text)
17585 .collect::<Vec<_>>(),
17586 vec!["method id() Now resolved!", "other"],
17587 "Should update first completion label, but not second as the filter text did not match."
17588 );
17589 }
17590 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17591 }
17592 });
17593}
17594
17595#[gpui::test]
17596async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17597 init_test(cx, |_| {});
17598 let mut cx = EditorLspTestContext::new_rust(
17599 lsp::ServerCapabilities {
17600 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17601 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17602 completion_provider: Some(lsp::CompletionOptions {
17603 resolve_provider: Some(true),
17604 ..Default::default()
17605 }),
17606 ..Default::default()
17607 },
17608 cx,
17609 )
17610 .await;
17611 cx.set_state(indoc! {"
17612 struct TestStruct {
17613 field: i32
17614 }
17615
17616 fn mainˇ() {
17617 let unused_var = 42;
17618 let test_struct = TestStruct { field: 42 };
17619 }
17620 "});
17621 let symbol_range = cx.lsp_range(indoc! {"
17622 struct TestStruct {
17623 field: i32
17624 }
17625
17626 «fn main»() {
17627 let unused_var = 42;
17628 let test_struct = TestStruct { field: 42 };
17629 }
17630 "});
17631 let mut hover_requests =
17632 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17633 Ok(Some(lsp::Hover {
17634 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17635 kind: lsp::MarkupKind::Markdown,
17636 value: "Function documentation".to_string(),
17637 }),
17638 range: Some(symbol_range),
17639 }))
17640 });
17641
17642 // Case 1: Test that code action menu hide hover popover
17643 cx.dispatch_action(Hover);
17644 hover_requests.next().await;
17645 cx.condition(|editor, _| editor.hover_state.visible()).await;
17646 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17647 move |_, _, _| async move {
17648 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17649 lsp::CodeAction {
17650 title: "Remove unused variable".to_string(),
17651 kind: Some(CodeActionKind::QUICKFIX),
17652 edit: Some(lsp::WorkspaceEdit {
17653 changes: Some(
17654 [(
17655 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17656 vec![lsp::TextEdit {
17657 range: lsp::Range::new(
17658 lsp::Position::new(5, 4),
17659 lsp::Position::new(5, 27),
17660 ),
17661 new_text: "".to_string(),
17662 }],
17663 )]
17664 .into_iter()
17665 .collect(),
17666 ),
17667 ..Default::default()
17668 }),
17669 ..Default::default()
17670 },
17671 )]))
17672 },
17673 );
17674 cx.update_editor(|editor, window, cx| {
17675 editor.toggle_code_actions(
17676 &ToggleCodeActions {
17677 deployed_from: None,
17678 quick_launch: false,
17679 },
17680 window,
17681 cx,
17682 );
17683 });
17684 code_action_requests.next().await;
17685 cx.run_until_parked();
17686 cx.condition(|editor, _| editor.context_menu_visible())
17687 .await;
17688 cx.update_editor(|editor, _, _| {
17689 assert!(
17690 !editor.hover_state.visible(),
17691 "Hover popover should be hidden when code action menu is shown"
17692 );
17693 // Hide code actions
17694 editor.context_menu.take();
17695 });
17696
17697 // Case 2: Test that code completions hide hover popover
17698 cx.dispatch_action(Hover);
17699 hover_requests.next().await;
17700 cx.condition(|editor, _| editor.hover_state.visible()).await;
17701 let counter = Arc::new(AtomicUsize::new(0));
17702 let mut completion_requests =
17703 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17704 let counter = counter.clone();
17705 async move {
17706 counter.fetch_add(1, atomic::Ordering::Release);
17707 Ok(Some(lsp::CompletionResponse::Array(vec![
17708 lsp::CompletionItem {
17709 label: "main".into(),
17710 kind: Some(lsp::CompletionItemKind::FUNCTION),
17711 detail: Some("() -> ()".to_string()),
17712 ..Default::default()
17713 },
17714 lsp::CompletionItem {
17715 label: "TestStruct".into(),
17716 kind: Some(lsp::CompletionItemKind::STRUCT),
17717 detail: Some("struct TestStruct".to_string()),
17718 ..Default::default()
17719 },
17720 ])))
17721 }
17722 });
17723 cx.update_editor(|editor, window, cx| {
17724 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17725 });
17726 completion_requests.next().await;
17727 cx.condition(|editor, _| editor.context_menu_visible())
17728 .await;
17729 cx.update_editor(|editor, _, _| {
17730 assert!(
17731 !editor.hover_state.visible(),
17732 "Hover popover should be hidden when completion menu is shown"
17733 );
17734 });
17735}
17736
17737#[gpui::test]
17738async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17739 init_test(cx, |_| {});
17740
17741 let mut cx = EditorLspTestContext::new_rust(
17742 lsp::ServerCapabilities {
17743 completion_provider: Some(lsp::CompletionOptions {
17744 trigger_characters: Some(vec![".".to_string()]),
17745 resolve_provider: Some(true),
17746 ..Default::default()
17747 }),
17748 ..Default::default()
17749 },
17750 cx,
17751 )
17752 .await;
17753
17754 cx.set_state("fn main() { let a = 2ˇ; }");
17755 cx.simulate_keystroke(".");
17756
17757 let unresolved_item_1 = lsp::CompletionItem {
17758 label: "id".to_string(),
17759 filter_text: Some("id".to_string()),
17760 detail: None,
17761 documentation: None,
17762 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17763 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17764 new_text: ".id".to_string(),
17765 })),
17766 ..lsp::CompletionItem::default()
17767 };
17768 let resolved_item_1 = lsp::CompletionItem {
17769 additional_text_edits: Some(vec![lsp::TextEdit {
17770 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17771 new_text: "!!".to_string(),
17772 }]),
17773 ..unresolved_item_1.clone()
17774 };
17775 let unresolved_item_2 = lsp::CompletionItem {
17776 label: "other".to_string(),
17777 filter_text: Some("other".to_string()),
17778 detail: None,
17779 documentation: None,
17780 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17781 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17782 new_text: ".other".to_string(),
17783 })),
17784 ..lsp::CompletionItem::default()
17785 };
17786 let resolved_item_2 = lsp::CompletionItem {
17787 additional_text_edits: Some(vec![lsp::TextEdit {
17788 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17789 new_text: "??".to_string(),
17790 }]),
17791 ..unresolved_item_2.clone()
17792 };
17793
17794 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17795 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17796 cx.lsp
17797 .server
17798 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17799 let unresolved_item_1 = unresolved_item_1.clone();
17800 let resolved_item_1 = resolved_item_1.clone();
17801 let unresolved_item_2 = unresolved_item_2.clone();
17802 let resolved_item_2 = resolved_item_2.clone();
17803 let resolve_requests_1 = resolve_requests_1.clone();
17804 let resolve_requests_2 = resolve_requests_2.clone();
17805 move |unresolved_request, _| {
17806 let unresolved_item_1 = unresolved_item_1.clone();
17807 let resolved_item_1 = resolved_item_1.clone();
17808 let unresolved_item_2 = unresolved_item_2.clone();
17809 let resolved_item_2 = resolved_item_2.clone();
17810 let resolve_requests_1 = resolve_requests_1.clone();
17811 let resolve_requests_2 = resolve_requests_2.clone();
17812 async move {
17813 if unresolved_request == unresolved_item_1 {
17814 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17815 Ok(resolved_item_1.clone())
17816 } else if unresolved_request == unresolved_item_2 {
17817 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17818 Ok(resolved_item_2.clone())
17819 } else {
17820 panic!("Unexpected completion item {unresolved_request:?}")
17821 }
17822 }
17823 }
17824 })
17825 .detach();
17826
17827 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17828 let unresolved_item_1 = unresolved_item_1.clone();
17829 let unresolved_item_2 = unresolved_item_2.clone();
17830 async move {
17831 Ok(Some(lsp::CompletionResponse::Array(vec![
17832 unresolved_item_1,
17833 unresolved_item_2,
17834 ])))
17835 }
17836 })
17837 .next()
17838 .await;
17839
17840 cx.condition(|editor, _| editor.context_menu_visible())
17841 .await;
17842 cx.update_editor(|editor, _, _| {
17843 let context_menu = editor.context_menu.borrow_mut();
17844 let context_menu = context_menu
17845 .as_ref()
17846 .expect("Should have the context menu deployed");
17847 match context_menu {
17848 CodeContextMenu::Completions(completions_menu) => {
17849 let completions = completions_menu.completions.borrow_mut();
17850 assert_eq!(
17851 completions
17852 .iter()
17853 .map(|completion| &completion.label.text)
17854 .collect::<Vec<_>>(),
17855 vec!["id", "other"]
17856 )
17857 }
17858 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17859 }
17860 });
17861 cx.run_until_parked();
17862
17863 cx.update_editor(|editor, window, cx| {
17864 editor.context_menu_next(&ContextMenuNext, window, cx);
17865 });
17866 cx.run_until_parked();
17867 cx.update_editor(|editor, window, cx| {
17868 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17869 });
17870 cx.run_until_parked();
17871 cx.update_editor(|editor, window, cx| {
17872 editor.context_menu_next(&ContextMenuNext, window, cx);
17873 });
17874 cx.run_until_parked();
17875 cx.update_editor(|editor, window, cx| {
17876 editor
17877 .compose_completion(&ComposeCompletion::default(), window, cx)
17878 .expect("No task returned")
17879 })
17880 .await
17881 .expect("Completion failed");
17882 cx.run_until_parked();
17883
17884 cx.update_editor(|editor, _, cx| {
17885 assert_eq!(
17886 resolve_requests_1.load(atomic::Ordering::Acquire),
17887 1,
17888 "Should always resolve once despite multiple selections"
17889 );
17890 assert_eq!(
17891 resolve_requests_2.load(atomic::Ordering::Acquire),
17892 1,
17893 "Should always resolve once after multiple selections and applying the completion"
17894 );
17895 assert_eq!(
17896 editor.text(cx),
17897 "fn main() { let a = ??.other; }",
17898 "Should use resolved data when applying the completion"
17899 );
17900 });
17901}
17902
17903#[gpui::test]
17904async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17905 init_test(cx, |_| {});
17906
17907 let item_0 = lsp::CompletionItem {
17908 label: "abs".into(),
17909 insert_text: Some("abs".into()),
17910 data: Some(json!({ "very": "special"})),
17911 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17912 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17913 lsp::InsertReplaceEdit {
17914 new_text: "abs".to_string(),
17915 insert: lsp::Range::default(),
17916 replace: lsp::Range::default(),
17917 },
17918 )),
17919 ..lsp::CompletionItem::default()
17920 };
17921 let items = iter::once(item_0.clone())
17922 .chain((11..51).map(|i| lsp::CompletionItem {
17923 label: format!("item_{}", i),
17924 insert_text: Some(format!("item_{}", i)),
17925 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17926 ..lsp::CompletionItem::default()
17927 }))
17928 .collect::<Vec<_>>();
17929
17930 let default_commit_characters = vec!["?".to_string()];
17931 let default_data = json!({ "default": "data"});
17932 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17933 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17934 let default_edit_range = lsp::Range {
17935 start: lsp::Position {
17936 line: 0,
17937 character: 5,
17938 },
17939 end: lsp::Position {
17940 line: 0,
17941 character: 5,
17942 },
17943 };
17944
17945 let mut cx = EditorLspTestContext::new_rust(
17946 lsp::ServerCapabilities {
17947 completion_provider: Some(lsp::CompletionOptions {
17948 trigger_characters: Some(vec![".".to_string()]),
17949 resolve_provider: Some(true),
17950 ..Default::default()
17951 }),
17952 ..Default::default()
17953 },
17954 cx,
17955 )
17956 .await;
17957
17958 cx.set_state("fn main() { let a = 2ˇ; }");
17959 cx.simulate_keystroke(".");
17960
17961 let completion_data = default_data.clone();
17962 let completion_characters = default_commit_characters.clone();
17963 let completion_items = items.clone();
17964 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17965 let default_data = completion_data.clone();
17966 let default_commit_characters = completion_characters.clone();
17967 let items = completion_items.clone();
17968 async move {
17969 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17970 items,
17971 item_defaults: Some(lsp::CompletionListItemDefaults {
17972 data: Some(default_data.clone()),
17973 commit_characters: Some(default_commit_characters.clone()),
17974 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17975 default_edit_range,
17976 )),
17977 insert_text_format: Some(default_insert_text_format),
17978 insert_text_mode: Some(default_insert_text_mode),
17979 }),
17980 ..lsp::CompletionList::default()
17981 })))
17982 }
17983 })
17984 .next()
17985 .await;
17986
17987 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17988 cx.lsp
17989 .server
17990 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17991 let closure_resolved_items = resolved_items.clone();
17992 move |item_to_resolve, _| {
17993 let closure_resolved_items = closure_resolved_items.clone();
17994 async move {
17995 closure_resolved_items.lock().push(item_to_resolve.clone());
17996 Ok(item_to_resolve)
17997 }
17998 }
17999 })
18000 .detach();
18001
18002 cx.condition(|editor, _| editor.context_menu_visible())
18003 .await;
18004 cx.run_until_parked();
18005 cx.update_editor(|editor, _, _| {
18006 let menu = editor.context_menu.borrow_mut();
18007 match menu.as_ref().expect("should have the completions menu") {
18008 CodeContextMenu::Completions(completions_menu) => {
18009 assert_eq!(
18010 completions_menu
18011 .entries
18012 .borrow()
18013 .iter()
18014 .map(|mat| mat.string.clone())
18015 .collect::<Vec<String>>(),
18016 items
18017 .iter()
18018 .map(|completion| completion.label.clone())
18019 .collect::<Vec<String>>()
18020 );
18021 }
18022 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18023 }
18024 });
18025 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18026 // with 4 from the end.
18027 assert_eq!(
18028 *resolved_items.lock(),
18029 [&items[0..16], &items[items.len() - 4..items.len()]]
18030 .concat()
18031 .iter()
18032 .cloned()
18033 .map(|mut item| {
18034 if item.data.is_none() {
18035 item.data = Some(default_data.clone());
18036 }
18037 item
18038 })
18039 .collect::<Vec<lsp::CompletionItem>>(),
18040 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18041 );
18042 resolved_items.lock().clear();
18043
18044 cx.update_editor(|editor, window, cx| {
18045 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18046 });
18047 cx.run_until_parked();
18048 // Completions that have already been resolved are skipped.
18049 assert_eq!(
18050 *resolved_items.lock(),
18051 items[items.len() - 17..items.len() - 4]
18052 .iter()
18053 .cloned()
18054 .map(|mut item| {
18055 if item.data.is_none() {
18056 item.data = Some(default_data.clone());
18057 }
18058 item
18059 })
18060 .collect::<Vec<lsp::CompletionItem>>()
18061 );
18062 resolved_items.lock().clear();
18063}
18064
18065#[gpui::test]
18066async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18067 init_test(cx, |_| {});
18068
18069 let mut cx = EditorLspTestContext::new(
18070 Language::new(
18071 LanguageConfig {
18072 matcher: LanguageMatcher {
18073 path_suffixes: vec!["jsx".into()],
18074 ..Default::default()
18075 },
18076 overrides: [(
18077 "element".into(),
18078 LanguageConfigOverride {
18079 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18080 ..Default::default()
18081 },
18082 )]
18083 .into_iter()
18084 .collect(),
18085 ..Default::default()
18086 },
18087 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18088 )
18089 .with_override_query("(jsx_self_closing_element) @element")
18090 .unwrap(),
18091 lsp::ServerCapabilities {
18092 completion_provider: Some(lsp::CompletionOptions {
18093 trigger_characters: Some(vec![":".to_string()]),
18094 ..Default::default()
18095 }),
18096 ..Default::default()
18097 },
18098 cx,
18099 )
18100 .await;
18101
18102 cx.lsp
18103 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18104 Ok(Some(lsp::CompletionResponse::Array(vec![
18105 lsp::CompletionItem {
18106 label: "bg-blue".into(),
18107 ..Default::default()
18108 },
18109 lsp::CompletionItem {
18110 label: "bg-red".into(),
18111 ..Default::default()
18112 },
18113 lsp::CompletionItem {
18114 label: "bg-yellow".into(),
18115 ..Default::default()
18116 },
18117 ])))
18118 });
18119
18120 cx.set_state(r#"<p class="bgˇ" />"#);
18121
18122 // Trigger completion when typing a dash, because the dash is an extra
18123 // word character in the 'element' scope, which contains the cursor.
18124 cx.simulate_keystroke("-");
18125 cx.executor().run_until_parked();
18126 cx.update_editor(|editor, _, _| {
18127 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18128 {
18129 assert_eq!(
18130 completion_menu_entries(menu),
18131 &["bg-blue", "bg-red", "bg-yellow"]
18132 );
18133 } else {
18134 panic!("expected completion menu to be open");
18135 }
18136 });
18137
18138 cx.simulate_keystroke("l");
18139 cx.executor().run_until_parked();
18140 cx.update_editor(|editor, _, _| {
18141 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18142 {
18143 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18144 } else {
18145 panic!("expected completion menu to be open");
18146 }
18147 });
18148
18149 // When filtering completions, consider the character after the '-' to
18150 // be the start of a subword.
18151 cx.set_state(r#"<p class="yelˇ" />"#);
18152 cx.simulate_keystroke("l");
18153 cx.executor().run_until_parked();
18154 cx.update_editor(|editor, _, _| {
18155 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18156 {
18157 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18158 } else {
18159 panic!("expected completion menu to be open");
18160 }
18161 });
18162}
18163
18164fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18165 let entries = menu.entries.borrow();
18166 entries.iter().map(|mat| mat.string.clone()).collect()
18167}
18168
18169#[gpui::test]
18170async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18171 init_test(cx, |settings| {
18172 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
18173 Formatter::Prettier,
18174 )))
18175 });
18176
18177 let fs = FakeFs::new(cx.executor());
18178 fs.insert_file(path!("/file.ts"), Default::default()).await;
18179
18180 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18181 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18182
18183 language_registry.add(Arc::new(Language::new(
18184 LanguageConfig {
18185 name: "TypeScript".into(),
18186 matcher: LanguageMatcher {
18187 path_suffixes: vec!["ts".to_string()],
18188 ..Default::default()
18189 },
18190 ..Default::default()
18191 },
18192 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18193 )));
18194 update_test_language_settings(cx, |settings| {
18195 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18196 });
18197
18198 let test_plugin = "test_plugin";
18199 let _ = language_registry.register_fake_lsp(
18200 "TypeScript",
18201 FakeLspAdapter {
18202 prettier_plugins: vec![test_plugin],
18203 ..Default::default()
18204 },
18205 );
18206
18207 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18208 let buffer = project
18209 .update(cx, |project, cx| {
18210 project.open_local_buffer(path!("/file.ts"), cx)
18211 })
18212 .await
18213 .unwrap();
18214
18215 let buffer_text = "one\ntwo\nthree\n";
18216 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18217 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18218 editor.update_in(cx, |editor, window, cx| {
18219 editor.set_text(buffer_text, window, cx)
18220 });
18221
18222 editor
18223 .update_in(cx, |editor, window, cx| {
18224 editor.perform_format(
18225 project.clone(),
18226 FormatTrigger::Manual,
18227 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18228 window,
18229 cx,
18230 )
18231 })
18232 .unwrap()
18233 .await;
18234 assert_eq!(
18235 editor.update(cx, |editor, cx| editor.text(cx)),
18236 buffer_text.to_string() + prettier_format_suffix,
18237 "Test prettier formatting was not applied to the original buffer text",
18238 );
18239
18240 update_test_language_settings(cx, |settings| {
18241 settings.defaults.formatter = Some(SelectedFormatter::Auto)
18242 });
18243 let format = editor.update_in(cx, |editor, window, cx| {
18244 editor.perform_format(
18245 project.clone(),
18246 FormatTrigger::Manual,
18247 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18248 window,
18249 cx,
18250 )
18251 });
18252 format.await.unwrap();
18253 assert_eq!(
18254 editor.update(cx, |editor, cx| editor.text(cx)),
18255 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18256 "Autoformatting (via test prettier) was not applied to the original buffer text",
18257 );
18258}
18259
18260#[gpui::test]
18261async fn test_addition_reverts(cx: &mut TestAppContext) {
18262 init_test(cx, |_| {});
18263 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18264 let base_text = indoc! {r#"
18265 struct Row;
18266 struct Row1;
18267 struct Row2;
18268
18269 struct Row4;
18270 struct Row5;
18271 struct Row6;
18272
18273 struct Row8;
18274 struct Row9;
18275 struct Row10;"#};
18276
18277 // When addition hunks are not adjacent to carets, no hunk revert is performed
18278 assert_hunk_revert(
18279 indoc! {r#"struct Row;
18280 struct Row1;
18281 struct Row1.1;
18282 struct Row1.2;
18283 struct Row2;ˇ
18284
18285 struct Row4;
18286 struct Row5;
18287 struct Row6;
18288
18289 struct Row8;
18290 ˇstruct Row9;
18291 struct Row9.1;
18292 struct Row9.2;
18293 struct Row9.3;
18294 struct Row10;"#},
18295 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18296 indoc! {r#"struct Row;
18297 struct Row1;
18298 struct Row1.1;
18299 struct Row1.2;
18300 struct Row2;ˇ
18301
18302 struct Row4;
18303 struct Row5;
18304 struct Row6;
18305
18306 struct Row8;
18307 ˇstruct Row9;
18308 struct Row9.1;
18309 struct Row9.2;
18310 struct Row9.3;
18311 struct Row10;"#},
18312 base_text,
18313 &mut cx,
18314 );
18315 // Same for selections
18316 assert_hunk_revert(
18317 indoc! {r#"struct Row;
18318 struct Row1;
18319 struct Row2;
18320 struct Row2.1;
18321 struct Row2.2;
18322 «ˇ
18323 struct Row4;
18324 struct» Row5;
18325 «struct Row6;
18326 ˇ»
18327 struct Row9.1;
18328 struct Row9.2;
18329 struct Row9.3;
18330 struct Row8;
18331 struct Row9;
18332 struct Row10;"#},
18333 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18334 indoc! {r#"struct Row;
18335 struct Row1;
18336 struct Row2;
18337 struct Row2.1;
18338 struct Row2.2;
18339 «ˇ
18340 struct Row4;
18341 struct» Row5;
18342 «struct Row6;
18343 ˇ»
18344 struct Row9.1;
18345 struct Row9.2;
18346 struct Row9.3;
18347 struct Row8;
18348 struct Row9;
18349 struct Row10;"#},
18350 base_text,
18351 &mut cx,
18352 );
18353
18354 // When carets and selections intersect the addition hunks, those are reverted.
18355 // Adjacent carets got merged.
18356 assert_hunk_revert(
18357 indoc! {r#"struct Row;
18358 ˇ// something on the top
18359 struct Row1;
18360 struct Row2;
18361 struct Roˇw3.1;
18362 struct Row2.2;
18363 struct Row2.3;ˇ
18364
18365 struct Row4;
18366 struct ˇRow5.1;
18367 struct Row5.2;
18368 struct «Rowˇ»5.3;
18369 struct Row5;
18370 struct Row6;
18371 ˇ
18372 struct Row9.1;
18373 struct «Rowˇ»9.2;
18374 struct «ˇRow»9.3;
18375 struct Row8;
18376 struct Row9;
18377 «ˇ// something on bottom»
18378 struct Row10;"#},
18379 vec![
18380 DiffHunkStatusKind::Added,
18381 DiffHunkStatusKind::Added,
18382 DiffHunkStatusKind::Added,
18383 DiffHunkStatusKind::Added,
18384 DiffHunkStatusKind::Added,
18385 ],
18386 indoc! {r#"struct Row;
18387 ˇstruct Row1;
18388 struct Row2;
18389 ˇ
18390 struct Row4;
18391 ˇstruct Row5;
18392 struct Row6;
18393 ˇ
18394 ˇstruct Row8;
18395 struct Row9;
18396 ˇstruct Row10;"#},
18397 base_text,
18398 &mut cx,
18399 );
18400}
18401
18402#[gpui::test]
18403async fn test_modification_reverts(cx: &mut TestAppContext) {
18404 init_test(cx, |_| {});
18405 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18406 let base_text = indoc! {r#"
18407 struct Row;
18408 struct Row1;
18409 struct Row2;
18410
18411 struct Row4;
18412 struct Row5;
18413 struct Row6;
18414
18415 struct Row8;
18416 struct Row9;
18417 struct Row10;"#};
18418
18419 // Modification hunks behave the same as the addition ones.
18420 assert_hunk_revert(
18421 indoc! {r#"struct Row;
18422 struct Row1;
18423 struct Row33;
18424 ˇ
18425 struct Row4;
18426 struct Row5;
18427 struct Row6;
18428 ˇ
18429 struct Row99;
18430 struct Row9;
18431 struct Row10;"#},
18432 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18433 indoc! {r#"struct Row;
18434 struct Row1;
18435 struct Row33;
18436 ˇ
18437 struct Row4;
18438 struct Row5;
18439 struct Row6;
18440 ˇ
18441 struct Row99;
18442 struct Row9;
18443 struct Row10;"#},
18444 base_text,
18445 &mut cx,
18446 );
18447 assert_hunk_revert(
18448 indoc! {r#"struct Row;
18449 struct Row1;
18450 struct Row33;
18451 «ˇ
18452 struct Row4;
18453 struct» Row5;
18454 «struct Row6;
18455 ˇ»
18456 struct Row99;
18457 struct Row9;
18458 struct Row10;"#},
18459 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18460 indoc! {r#"struct Row;
18461 struct Row1;
18462 struct Row33;
18463 «ˇ
18464 struct Row4;
18465 struct» Row5;
18466 «struct Row6;
18467 ˇ»
18468 struct Row99;
18469 struct Row9;
18470 struct Row10;"#},
18471 base_text,
18472 &mut cx,
18473 );
18474
18475 assert_hunk_revert(
18476 indoc! {r#"ˇstruct Row1.1;
18477 struct Row1;
18478 «ˇstr»uct Row22;
18479
18480 struct ˇRow44;
18481 struct Row5;
18482 struct «Rˇ»ow66;ˇ
18483
18484 «struˇ»ct Row88;
18485 struct Row9;
18486 struct Row1011;ˇ"#},
18487 vec![
18488 DiffHunkStatusKind::Modified,
18489 DiffHunkStatusKind::Modified,
18490 DiffHunkStatusKind::Modified,
18491 DiffHunkStatusKind::Modified,
18492 DiffHunkStatusKind::Modified,
18493 DiffHunkStatusKind::Modified,
18494 ],
18495 indoc! {r#"struct Row;
18496 ˇstruct Row1;
18497 struct Row2;
18498 ˇ
18499 struct Row4;
18500 ˇstruct Row5;
18501 struct Row6;
18502 ˇ
18503 struct Row8;
18504 ˇstruct Row9;
18505 struct Row10;ˇ"#},
18506 base_text,
18507 &mut cx,
18508 );
18509}
18510
18511#[gpui::test]
18512async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18513 init_test(cx, |_| {});
18514 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18515 let base_text = indoc! {r#"
18516 one
18517
18518 two
18519 three
18520 "#};
18521
18522 cx.set_head_text(base_text);
18523 cx.set_state("\nˇ\n");
18524 cx.executor().run_until_parked();
18525 cx.update_editor(|editor, _window, cx| {
18526 editor.expand_selected_diff_hunks(cx);
18527 });
18528 cx.executor().run_until_parked();
18529 cx.update_editor(|editor, window, cx| {
18530 editor.backspace(&Default::default(), window, cx);
18531 });
18532 cx.run_until_parked();
18533 cx.assert_state_with_diff(
18534 indoc! {r#"
18535
18536 - two
18537 - threeˇ
18538 +
18539 "#}
18540 .to_string(),
18541 );
18542}
18543
18544#[gpui::test]
18545async fn test_deletion_reverts(cx: &mut TestAppContext) {
18546 init_test(cx, |_| {});
18547 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18548 let base_text = indoc! {r#"struct Row;
18549struct Row1;
18550struct Row2;
18551
18552struct Row4;
18553struct Row5;
18554struct Row6;
18555
18556struct Row8;
18557struct Row9;
18558struct Row10;"#};
18559
18560 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18561 assert_hunk_revert(
18562 indoc! {r#"struct Row;
18563 struct Row2;
18564
18565 ˇstruct Row4;
18566 struct Row5;
18567 struct Row6;
18568 ˇ
18569 struct Row8;
18570 struct Row10;"#},
18571 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
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 base_text,
18582 &mut cx,
18583 );
18584 assert_hunk_revert(
18585 indoc! {r#"struct Row;
18586 struct Row2;
18587
18588 «ˇstruct Row4;
18589 struct» Row5;
18590 «struct Row6;
18591 ˇ»
18592 struct Row8;
18593 struct Row10;"#},
18594 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
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 base_text,
18605 &mut cx,
18606 );
18607
18608 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18609 assert_hunk_revert(
18610 indoc! {r#"struct Row;
18611 ˇstruct Row2;
18612
18613 struct Row4;
18614 struct Row5;
18615 struct Row6;
18616
18617 struct Row8;ˇ
18618 struct Row10;"#},
18619 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18620 indoc! {r#"struct Row;
18621 struct Row1;
18622 ˇstruct Row2;
18623
18624 struct Row4;
18625 struct Row5;
18626 struct Row6;
18627
18628 struct Row8;ˇ
18629 struct Row9;
18630 struct Row10;"#},
18631 base_text,
18632 &mut cx,
18633 );
18634 assert_hunk_revert(
18635 indoc! {r#"struct Row;
18636 struct Row2«ˇ;
18637 struct Row4;
18638 struct» Row5;
18639 «struct Row6;
18640
18641 struct Row8;ˇ»
18642 struct Row10;"#},
18643 vec![
18644 DiffHunkStatusKind::Deleted,
18645 DiffHunkStatusKind::Deleted,
18646 DiffHunkStatusKind::Deleted,
18647 ],
18648 indoc! {r#"struct Row;
18649 struct Row1;
18650 struct Row2«ˇ;
18651
18652 struct Row4;
18653 struct» Row5;
18654 «struct Row6;
18655
18656 struct Row8;ˇ»
18657 struct Row9;
18658 struct Row10;"#},
18659 base_text,
18660 &mut cx,
18661 );
18662}
18663
18664#[gpui::test]
18665async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18666 init_test(cx, |_| {});
18667
18668 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18669 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18670 let base_text_3 =
18671 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18672
18673 let text_1 = edit_first_char_of_every_line(base_text_1);
18674 let text_2 = edit_first_char_of_every_line(base_text_2);
18675 let text_3 = edit_first_char_of_every_line(base_text_3);
18676
18677 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18678 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18679 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18680
18681 let multibuffer = cx.new(|cx| {
18682 let mut multibuffer = MultiBuffer::new(ReadWrite);
18683 multibuffer.push_excerpts(
18684 buffer_1.clone(),
18685 [
18686 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18687 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18688 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18689 ],
18690 cx,
18691 );
18692 multibuffer.push_excerpts(
18693 buffer_2.clone(),
18694 [
18695 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18696 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18697 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18698 ],
18699 cx,
18700 );
18701 multibuffer.push_excerpts(
18702 buffer_3.clone(),
18703 [
18704 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18705 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18706 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18707 ],
18708 cx,
18709 );
18710 multibuffer
18711 });
18712
18713 let fs = FakeFs::new(cx.executor());
18714 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18715 let (editor, cx) = cx
18716 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18717 editor.update_in(cx, |editor, _window, cx| {
18718 for (buffer, diff_base) in [
18719 (buffer_1.clone(), base_text_1),
18720 (buffer_2.clone(), base_text_2),
18721 (buffer_3.clone(), base_text_3),
18722 ] {
18723 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18724 editor
18725 .buffer
18726 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18727 }
18728 });
18729 cx.executor().run_until_parked();
18730
18731 editor.update_in(cx, |editor, window, cx| {
18732 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}");
18733 editor.select_all(&SelectAll, window, cx);
18734 editor.git_restore(&Default::default(), window, cx);
18735 });
18736 cx.executor().run_until_parked();
18737
18738 // When all ranges are selected, all buffer hunks are reverted.
18739 editor.update(cx, |editor, cx| {
18740 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");
18741 });
18742 buffer_1.update(cx, |buffer, _| {
18743 assert_eq!(buffer.text(), base_text_1);
18744 });
18745 buffer_2.update(cx, |buffer, _| {
18746 assert_eq!(buffer.text(), base_text_2);
18747 });
18748 buffer_3.update(cx, |buffer, _| {
18749 assert_eq!(buffer.text(), base_text_3);
18750 });
18751
18752 editor.update_in(cx, |editor, window, cx| {
18753 editor.undo(&Default::default(), window, cx);
18754 });
18755
18756 editor.update_in(cx, |editor, window, cx| {
18757 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18758 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18759 });
18760 editor.git_restore(&Default::default(), window, cx);
18761 });
18762
18763 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18764 // but not affect buffer_2 and its related excerpts.
18765 editor.update(cx, |editor, cx| {
18766 assert_eq!(
18767 editor.text(cx),
18768 "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}"
18769 );
18770 });
18771 buffer_1.update(cx, |buffer, _| {
18772 assert_eq!(buffer.text(), base_text_1);
18773 });
18774 buffer_2.update(cx, |buffer, _| {
18775 assert_eq!(
18776 buffer.text(),
18777 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18778 );
18779 });
18780 buffer_3.update(cx, |buffer, _| {
18781 assert_eq!(
18782 buffer.text(),
18783 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18784 );
18785 });
18786
18787 fn edit_first_char_of_every_line(text: &str) -> String {
18788 text.split('\n')
18789 .map(|line| format!("X{}", &line[1..]))
18790 .collect::<Vec<_>>()
18791 .join("\n")
18792 }
18793}
18794
18795#[gpui::test]
18796async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18797 init_test(cx, |_| {});
18798
18799 let cols = 4;
18800 let rows = 10;
18801 let sample_text_1 = sample_text(rows, cols, 'a');
18802 assert_eq!(
18803 sample_text_1,
18804 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18805 );
18806 let sample_text_2 = sample_text(rows, cols, 'l');
18807 assert_eq!(
18808 sample_text_2,
18809 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18810 );
18811 let sample_text_3 = sample_text(rows, cols, 'v');
18812 assert_eq!(
18813 sample_text_3,
18814 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18815 );
18816
18817 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18818 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18819 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18820
18821 let multi_buffer = cx.new(|cx| {
18822 let mut multibuffer = MultiBuffer::new(ReadWrite);
18823 multibuffer.push_excerpts(
18824 buffer_1.clone(),
18825 [
18826 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18827 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18828 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18829 ],
18830 cx,
18831 );
18832 multibuffer.push_excerpts(
18833 buffer_2.clone(),
18834 [
18835 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18836 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18837 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18838 ],
18839 cx,
18840 );
18841 multibuffer.push_excerpts(
18842 buffer_3.clone(),
18843 [
18844 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18845 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18846 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18847 ],
18848 cx,
18849 );
18850 multibuffer
18851 });
18852
18853 let fs = FakeFs::new(cx.executor());
18854 fs.insert_tree(
18855 "/a",
18856 json!({
18857 "main.rs": sample_text_1,
18858 "other.rs": sample_text_2,
18859 "lib.rs": sample_text_3,
18860 }),
18861 )
18862 .await;
18863 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18864 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18865 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18866 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18867 Editor::new(
18868 EditorMode::full(),
18869 multi_buffer,
18870 Some(project.clone()),
18871 window,
18872 cx,
18873 )
18874 });
18875 let multibuffer_item_id = workspace
18876 .update(cx, |workspace, window, cx| {
18877 assert!(
18878 workspace.active_item(cx).is_none(),
18879 "active item should be None before the first item is added"
18880 );
18881 workspace.add_item_to_active_pane(
18882 Box::new(multi_buffer_editor.clone()),
18883 None,
18884 true,
18885 window,
18886 cx,
18887 );
18888 let active_item = workspace
18889 .active_item(cx)
18890 .expect("should have an active item after adding the multi buffer");
18891 assert_eq!(
18892 active_item.buffer_kind(cx),
18893 ItemBufferKind::Multibuffer,
18894 "A multi buffer was expected to active after adding"
18895 );
18896 active_item.item_id()
18897 })
18898 .unwrap();
18899 cx.executor().run_until_parked();
18900
18901 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18902 editor.change_selections(
18903 SelectionEffects::scroll(Autoscroll::Next),
18904 window,
18905 cx,
18906 |s| s.select_ranges(Some(1..2)),
18907 );
18908 editor.open_excerpts(&OpenExcerpts, window, cx);
18909 });
18910 cx.executor().run_until_parked();
18911 let first_item_id = workspace
18912 .update(cx, |workspace, window, cx| {
18913 let active_item = workspace
18914 .active_item(cx)
18915 .expect("should have an active item after navigating into the 1st buffer");
18916 let first_item_id = active_item.item_id();
18917 assert_ne!(
18918 first_item_id, multibuffer_item_id,
18919 "Should navigate into the 1st buffer and activate it"
18920 );
18921 assert_eq!(
18922 active_item.buffer_kind(cx),
18923 ItemBufferKind::Singleton,
18924 "New active item should be a singleton buffer"
18925 );
18926 assert_eq!(
18927 active_item
18928 .act_as::<Editor>(cx)
18929 .expect("should have navigated into an editor for the 1st buffer")
18930 .read(cx)
18931 .text(cx),
18932 sample_text_1
18933 );
18934
18935 workspace
18936 .go_back(workspace.active_pane().downgrade(), window, cx)
18937 .detach_and_log_err(cx);
18938
18939 first_item_id
18940 })
18941 .unwrap();
18942 cx.executor().run_until_parked();
18943 workspace
18944 .update(cx, |workspace, _, cx| {
18945 let active_item = workspace
18946 .active_item(cx)
18947 .expect("should have an active item after navigating back");
18948 assert_eq!(
18949 active_item.item_id(),
18950 multibuffer_item_id,
18951 "Should navigate back to the multi buffer"
18952 );
18953 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
18954 })
18955 .unwrap();
18956
18957 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18958 editor.change_selections(
18959 SelectionEffects::scroll(Autoscroll::Next),
18960 window,
18961 cx,
18962 |s| s.select_ranges(Some(39..40)),
18963 );
18964 editor.open_excerpts(&OpenExcerpts, window, cx);
18965 });
18966 cx.executor().run_until_parked();
18967 let second_item_id = workspace
18968 .update(cx, |workspace, window, cx| {
18969 let active_item = workspace
18970 .active_item(cx)
18971 .expect("should have an active item after navigating into the 2nd buffer");
18972 let second_item_id = active_item.item_id();
18973 assert_ne!(
18974 second_item_id, multibuffer_item_id,
18975 "Should navigate away from the multibuffer"
18976 );
18977 assert_ne!(
18978 second_item_id, first_item_id,
18979 "Should navigate into the 2nd buffer and activate it"
18980 );
18981 assert_eq!(
18982 active_item.buffer_kind(cx),
18983 ItemBufferKind::Singleton,
18984 "New active item should be a singleton buffer"
18985 );
18986 assert_eq!(
18987 active_item
18988 .act_as::<Editor>(cx)
18989 .expect("should have navigated into an editor")
18990 .read(cx)
18991 .text(cx),
18992 sample_text_2
18993 );
18994
18995 workspace
18996 .go_back(workspace.active_pane().downgrade(), window, cx)
18997 .detach_and_log_err(cx);
18998
18999 second_item_id
19000 })
19001 .unwrap();
19002 cx.executor().run_until_parked();
19003 workspace
19004 .update(cx, |workspace, _, cx| {
19005 let active_item = workspace
19006 .active_item(cx)
19007 .expect("should have an active item after navigating back from the 2nd buffer");
19008 assert_eq!(
19009 active_item.item_id(),
19010 multibuffer_item_id,
19011 "Should navigate back from the 2nd buffer to the multi buffer"
19012 );
19013 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19014 })
19015 .unwrap();
19016
19017 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19018 editor.change_selections(
19019 SelectionEffects::scroll(Autoscroll::Next),
19020 window,
19021 cx,
19022 |s| s.select_ranges(Some(70..70)),
19023 );
19024 editor.open_excerpts(&OpenExcerpts, window, cx);
19025 });
19026 cx.executor().run_until_parked();
19027 workspace
19028 .update(cx, |workspace, window, cx| {
19029 let active_item = workspace
19030 .active_item(cx)
19031 .expect("should have an active item after navigating into the 3rd buffer");
19032 let third_item_id = active_item.item_id();
19033 assert_ne!(
19034 third_item_id, multibuffer_item_id,
19035 "Should navigate into the 3rd buffer and activate it"
19036 );
19037 assert_ne!(third_item_id, first_item_id);
19038 assert_ne!(third_item_id, second_item_id);
19039 assert_eq!(
19040 active_item.buffer_kind(cx),
19041 ItemBufferKind::Singleton,
19042 "New active item should be a singleton buffer"
19043 );
19044 assert_eq!(
19045 active_item
19046 .act_as::<Editor>(cx)
19047 .expect("should have navigated into an editor")
19048 .read(cx)
19049 .text(cx),
19050 sample_text_3
19051 );
19052
19053 workspace
19054 .go_back(workspace.active_pane().downgrade(), window, cx)
19055 .detach_and_log_err(cx);
19056 })
19057 .unwrap();
19058 cx.executor().run_until_parked();
19059 workspace
19060 .update(cx, |workspace, _, cx| {
19061 let active_item = workspace
19062 .active_item(cx)
19063 .expect("should have an active item after navigating back from the 3rd buffer");
19064 assert_eq!(
19065 active_item.item_id(),
19066 multibuffer_item_id,
19067 "Should navigate back from the 3rd buffer to the multi buffer"
19068 );
19069 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19070 })
19071 .unwrap();
19072}
19073
19074#[gpui::test]
19075async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19076 init_test(cx, |_| {});
19077
19078 let mut cx = EditorTestContext::new(cx).await;
19079
19080 let diff_base = r#"
19081 use some::mod;
19082
19083 const A: u32 = 42;
19084
19085 fn main() {
19086 println!("hello");
19087
19088 println!("world");
19089 }
19090 "#
19091 .unindent();
19092
19093 cx.set_state(
19094 &r#"
19095 use some::modified;
19096
19097 ˇ
19098 fn main() {
19099 println!("hello there");
19100
19101 println!("around the");
19102 println!("world");
19103 }
19104 "#
19105 .unindent(),
19106 );
19107
19108 cx.set_head_text(&diff_base);
19109 executor.run_until_parked();
19110
19111 cx.update_editor(|editor, window, cx| {
19112 editor.go_to_next_hunk(&GoToHunk, window, cx);
19113 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19114 });
19115 executor.run_until_parked();
19116 cx.assert_state_with_diff(
19117 r#"
19118 use some::modified;
19119
19120
19121 fn main() {
19122 - println!("hello");
19123 + ˇ println!("hello there");
19124
19125 println!("around the");
19126 println!("world");
19127 }
19128 "#
19129 .unindent(),
19130 );
19131
19132 cx.update_editor(|editor, window, cx| {
19133 for _ in 0..2 {
19134 editor.go_to_next_hunk(&GoToHunk, window, cx);
19135 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19136 }
19137 });
19138 executor.run_until_parked();
19139 cx.assert_state_with_diff(
19140 r#"
19141 - use some::mod;
19142 + ˇuse some::modified;
19143
19144
19145 fn main() {
19146 - println!("hello");
19147 + println!("hello there");
19148
19149 + println!("around the");
19150 println!("world");
19151 }
19152 "#
19153 .unindent(),
19154 );
19155
19156 cx.update_editor(|editor, window, cx| {
19157 editor.go_to_next_hunk(&GoToHunk, window, cx);
19158 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19159 });
19160 executor.run_until_parked();
19161 cx.assert_state_with_diff(
19162 r#"
19163 - use some::mod;
19164 + use some::modified;
19165
19166 - const A: u32 = 42;
19167 ˇ
19168 fn main() {
19169 - println!("hello");
19170 + println!("hello there");
19171
19172 + println!("around the");
19173 println!("world");
19174 }
19175 "#
19176 .unindent(),
19177 );
19178
19179 cx.update_editor(|editor, window, cx| {
19180 editor.cancel(&Cancel, window, cx);
19181 });
19182
19183 cx.assert_state_with_diff(
19184 r#"
19185 use some::modified;
19186
19187 ˇ
19188 fn main() {
19189 println!("hello there");
19190
19191 println!("around the");
19192 println!("world");
19193 }
19194 "#
19195 .unindent(),
19196 );
19197}
19198
19199#[gpui::test]
19200async fn test_diff_base_change_with_expanded_diff_hunks(
19201 executor: BackgroundExecutor,
19202 cx: &mut TestAppContext,
19203) {
19204 init_test(cx, |_| {});
19205
19206 let mut cx = EditorTestContext::new(cx).await;
19207
19208 let diff_base = r#"
19209 use some::mod1;
19210 use some::mod2;
19211
19212 const A: u32 = 42;
19213 const B: u32 = 42;
19214 const C: u32 = 42;
19215
19216 fn main() {
19217 println!("hello");
19218
19219 println!("world");
19220 }
19221 "#
19222 .unindent();
19223
19224 cx.set_state(
19225 &r#"
19226 use some::mod2;
19227
19228 const A: u32 = 42;
19229 const C: u32 = 42;
19230
19231 fn main(ˇ) {
19232 //println!("hello");
19233
19234 println!("world");
19235 //
19236 //
19237 }
19238 "#
19239 .unindent(),
19240 );
19241
19242 cx.set_head_text(&diff_base);
19243 executor.run_until_parked();
19244
19245 cx.update_editor(|editor, window, cx| {
19246 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19247 });
19248 executor.run_until_parked();
19249 cx.assert_state_with_diff(
19250 r#"
19251 - use some::mod1;
19252 use some::mod2;
19253
19254 const A: u32 = 42;
19255 - const B: u32 = 42;
19256 const C: u32 = 42;
19257
19258 fn main(ˇ) {
19259 - println!("hello");
19260 + //println!("hello");
19261
19262 println!("world");
19263 + //
19264 + //
19265 }
19266 "#
19267 .unindent(),
19268 );
19269
19270 cx.set_head_text("new diff base!");
19271 executor.run_until_parked();
19272 cx.assert_state_with_diff(
19273 r#"
19274 - new diff base!
19275 + use some::mod2;
19276 +
19277 + const A: u32 = 42;
19278 + const C: u32 = 42;
19279 +
19280 + fn main(ˇ) {
19281 + //println!("hello");
19282 +
19283 + println!("world");
19284 + //
19285 + //
19286 + }
19287 "#
19288 .unindent(),
19289 );
19290}
19291
19292#[gpui::test]
19293async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19294 init_test(cx, |_| {});
19295
19296 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19297 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19298 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19299 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19300 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19301 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19302
19303 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19304 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19305 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19306
19307 let multi_buffer = cx.new(|cx| {
19308 let mut multibuffer = MultiBuffer::new(ReadWrite);
19309 multibuffer.push_excerpts(
19310 buffer_1.clone(),
19311 [
19312 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19313 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19314 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19315 ],
19316 cx,
19317 );
19318 multibuffer.push_excerpts(
19319 buffer_2.clone(),
19320 [
19321 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19322 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19323 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19324 ],
19325 cx,
19326 );
19327 multibuffer.push_excerpts(
19328 buffer_3.clone(),
19329 [
19330 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19331 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19332 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19333 ],
19334 cx,
19335 );
19336 multibuffer
19337 });
19338
19339 let editor =
19340 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19341 editor
19342 .update(cx, |editor, _window, cx| {
19343 for (buffer, diff_base) in [
19344 (buffer_1.clone(), file_1_old),
19345 (buffer_2.clone(), file_2_old),
19346 (buffer_3.clone(), file_3_old),
19347 ] {
19348 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19349 editor
19350 .buffer
19351 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19352 }
19353 })
19354 .unwrap();
19355
19356 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19357 cx.run_until_parked();
19358
19359 cx.assert_editor_state(
19360 &"
19361 ˇaaa
19362 ccc
19363 ddd
19364
19365 ggg
19366 hhh
19367
19368
19369 lll
19370 mmm
19371 NNN
19372
19373 qqq
19374 rrr
19375
19376 uuu
19377 111
19378 222
19379 333
19380
19381 666
19382 777
19383
19384 000
19385 !!!"
19386 .unindent(),
19387 );
19388
19389 cx.update_editor(|editor, window, cx| {
19390 editor.select_all(&SelectAll, window, cx);
19391 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19392 });
19393 cx.executor().run_until_parked();
19394
19395 cx.assert_state_with_diff(
19396 "
19397 «aaa
19398 - bbb
19399 ccc
19400 ddd
19401
19402 ggg
19403 hhh
19404
19405
19406 lll
19407 mmm
19408 - nnn
19409 + NNN
19410
19411 qqq
19412 rrr
19413
19414 uuu
19415 111
19416 222
19417 333
19418
19419 + 666
19420 777
19421
19422 000
19423 !!!ˇ»"
19424 .unindent(),
19425 );
19426}
19427
19428#[gpui::test]
19429async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19430 init_test(cx, |_| {});
19431
19432 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19433 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19434
19435 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19436 let multi_buffer = cx.new(|cx| {
19437 let mut multibuffer = MultiBuffer::new(ReadWrite);
19438 multibuffer.push_excerpts(
19439 buffer.clone(),
19440 [
19441 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19442 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19443 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19444 ],
19445 cx,
19446 );
19447 multibuffer
19448 });
19449
19450 let editor =
19451 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19452 editor
19453 .update(cx, |editor, _window, cx| {
19454 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19455 editor
19456 .buffer
19457 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19458 })
19459 .unwrap();
19460
19461 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19462 cx.run_until_parked();
19463
19464 cx.update_editor(|editor, window, cx| {
19465 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19466 });
19467 cx.executor().run_until_parked();
19468
19469 // When the start of a hunk coincides with the start of its excerpt,
19470 // the hunk is expanded. When the start of a hunk is earlier than
19471 // the start of its excerpt, the hunk is not expanded.
19472 cx.assert_state_with_diff(
19473 "
19474 ˇaaa
19475 - bbb
19476 + BBB
19477
19478 - ddd
19479 - eee
19480 + DDD
19481 + EEE
19482 fff
19483
19484 iii
19485 "
19486 .unindent(),
19487 );
19488}
19489
19490#[gpui::test]
19491async fn test_edits_around_expanded_insertion_hunks(
19492 executor: BackgroundExecutor,
19493 cx: &mut TestAppContext,
19494) {
19495 init_test(cx, |_| {});
19496
19497 let mut cx = EditorTestContext::new(cx).await;
19498
19499 let diff_base = r#"
19500 use some::mod1;
19501 use some::mod2;
19502
19503 const A: u32 = 42;
19504
19505 fn main() {
19506 println!("hello");
19507
19508 println!("world");
19509 }
19510 "#
19511 .unindent();
19512 executor.run_until_parked();
19513 cx.set_state(
19514 &r#"
19515 use some::mod1;
19516 use some::mod2;
19517
19518 const A: u32 = 42;
19519 const B: u32 = 42;
19520 const C: u32 = 42;
19521 ˇ
19522
19523 fn main() {
19524 println!("hello");
19525
19526 println!("world");
19527 }
19528 "#
19529 .unindent(),
19530 );
19531
19532 cx.set_head_text(&diff_base);
19533 executor.run_until_parked();
19534
19535 cx.update_editor(|editor, window, cx| {
19536 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19537 });
19538 executor.run_until_parked();
19539
19540 cx.assert_state_with_diff(
19541 r#"
19542 use some::mod1;
19543 use some::mod2;
19544
19545 const A: u32 = 42;
19546 + const B: u32 = 42;
19547 + const C: u32 = 42;
19548 + ˇ
19549
19550 fn main() {
19551 println!("hello");
19552
19553 println!("world");
19554 }
19555 "#
19556 .unindent(),
19557 );
19558
19559 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19560 executor.run_until_parked();
19561
19562 cx.assert_state_with_diff(
19563 r#"
19564 use some::mod1;
19565 use some::mod2;
19566
19567 const A: u32 = 42;
19568 + const B: u32 = 42;
19569 + const C: u32 = 42;
19570 + const D: u32 = 42;
19571 + ˇ
19572
19573 fn main() {
19574 println!("hello");
19575
19576 println!("world");
19577 }
19578 "#
19579 .unindent(),
19580 );
19581
19582 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19583 executor.run_until_parked();
19584
19585 cx.assert_state_with_diff(
19586 r#"
19587 use some::mod1;
19588 use some::mod2;
19589
19590 const A: u32 = 42;
19591 + const B: u32 = 42;
19592 + const C: u32 = 42;
19593 + const D: u32 = 42;
19594 + const E: u32 = 42;
19595 + ˇ
19596
19597 fn main() {
19598 println!("hello");
19599
19600 println!("world");
19601 }
19602 "#
19603 .unindent(),
19604 );
19605
19606 cx.update_editor(|editor, window, cx| {
19607 editor.delete_line(&DeleteLine, window, cx);
19608 });
19609 executor.run_until_parked();
19610
19611 cx.assert_state_with_diff(
19612 r#"
19613 use some::mod1;
19614 use some::mod2;
19615
19616 const A: u32 = 42;
19617 + const B: u32 = 42;
19618 + const C: u32 = 42;
19619 + const D: u32 = 42;
19620 + const E: u32 = 42;
19621 ˇ
19622 fn main() {
19623 println!("hello");
19624
19625 println!("world");
19626 }
19627 "#
19628 .unindent(),
19629 );
19630
19631 cx.update_editor(|editor, window, cx| {
19632 editor.move_up(&MoveUp, window, cx);
19633 editor.delete_line(&DeleteLine, window, cx);
19634 editor.move_up(&MoveUp, window, cx);
19635 editor.delete_line(&DeleteLine, window, cx);
19636 editor.move_up(&MoveUp, window, cx);
19637 editor.delete_line(&DeleteLine, window, cx);
19638 });
19639 executor.run_until_parked();
19640 cx.assert_state_with_diff(
19641 r#"
19642 use some::mod1;
19643 use some::mod2;
19644
19645 const A: u32 = 42;
19646 + const B: u32 = 42;
19647 ˇ
19648 fn main() {
19649 println!("hello");
19650
19651 println!("world");
19652 }
19653 "#
19654 .unindent(),
19655 );
19656
19657 cx.update_editor(|editor, window, cx| {
19658 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19659 editor.delete_line(&DeleteLine, window, cx);
19660 });
19661 executor.run_until_parked();
19662 cx.assert_state_with_diff(
19663 r#"
19664 ˇ
19665 fn main() {
19666 println!("hello");
19667
19668 println!("world");
19669 }
19670 "#
19671 .unindent(),
19672 );
19673}
19674
19675#[gpui::test]
19676async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19677 init_test(cx, |_| {});
19678
19679 let mut cx = EditorTestContext::new(cx).await;
19680 cx.set_head_text(indoc! { "
19681 one
19682 two
19683 three
19684 four
19685 five
19686 "
19687 });
19688 cx.set_state(indoc! { "
19689 one
19690 ˇthree
19691 five
19692 "});
19693 cx.run_until_parked();
19694 cx.update_editor(|editor, window, cx| {
19695 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19696 });
19697 cx.assert_state_with_diff(
19698 indoc! { "
19699 one
19700 - two
19701 ˇthree
19702 - four
19703 five
19704 "}
19705 .to_string(),
19706 );
19707 cx.update_editor(|editor, window, cx| {
19708 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19709 });
19710
19711 cx.assert_state_with_diff(
19712 indoc! { "
19713 one
19714 ˇthree
19715 five
19716 "}
19717 .to_string(),
19718 );
19719
19720 cx.set_state(indoc! { "
19721 one
19722 ˇTWO
19723 three
19724 four
19725 five
19726 "});
19727 cx.run_until_parked();
19728 cx.update_editor(|editor, window, cx| {
19729 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19730 });
19731
19732 cx.assert_state_with_diff(
19733 indoc! { "
19734 one
19735 - two
19736 + ˇTWO
19737 three
19738 four
19739 five
19740 "}
19741 .to_string(),
19742 );
19743 cx.update_editor(|editor, window, cx| {
19744 editor.move_up(&Default::default(), window, cx);
19745 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19746 });
19747 cx.assert_state_with_diff(
19748 indoc! { "
19749 one
19750 ˇTWO
19751 three
19752 four
19753 five
19754 "}
19755 .to_string(),
19756 );
19757}
19758
19759#[gpui::test]
19760async fn test_edits_around_expanded_deletion_hunks(
19761 executor: BackgroundExecutor,
19762 cx: &mut TestAppContext,
19763) {
19764 init_test(cx, |_| {});
19765
19766 let mut cx = EditorTestContext::new(cx).await;
19767
19768 let diff_base = r#"
19769 use some::mod1;
19770 use some::mod2;
19771
19772 const A: u32 = 42;
19773 const B: u32 = 42;
19774 const C: u32 = 42;
19775
19776
19777 fn main() {
19778 println!("hello");
19779
19780 println!("world");
19781 }
19782 "#
19783 .unindent();
19784 executor.run_until_parked();
19785 cx.set_state(
19786 &r#"
19787 use some::mod1;
19788 use some::mod2;
19789
19790 ˇconst B: u32 = 42;
19791 const C: u32 = 42;
19792
19793
19794 fn main() {
19795 println!("hello");
19796
19797 println!("world");
19798 }
19799 "#
19800 .unindent(),
19801 );
19802
19803 cx.set_head_text(&diff_base);
19804 executor.run_until_parked();
19805
19806 cx.update_editor(|editor, window, cx| {
19807 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19808 });
19809 executor.run_until_parked();
19810
19811 cx.assert_state_with_diff(
19812 r#"
19813 use some::mod1;
19814 use some::mod2;
19815
19816 - const A: u32 = 42;
19817 ˇconst B: u32 = 42;
19818 const C: u32 = 42;
19819
19820
19821 fn main() {
19822 println!("hello");
19823
19824 println!("world");
19825 }
19826 "#
19827 .unindent(),
19828 );
19829
19830 cx.update_editor(|editor, window, cx| {
19831 editor.delete_line(&DeleteLine, window, cx);
19832 });
19833 executor.run_until_parked();
19834 cx.assert_state_with_diff(
19835 r#"
19836 use some::mod1;
19837 use some::mod2;
19838
19839 - const A: u32 = 42;
19840 - const B: u32 = 42;
19841 ˇconst C: u32 = 42;
19842
19843
19844 fn main() {
19845 println!("hello");
19846
19847 println!("world");
19848 }
19849 "#
19850 .unindent(),
19851 );
19852
19853 cx.update_editor(|editor, window, cx| {
19854 editor.delete_line(&DeleteLine, window, cx);
19855 });
19856 executor.run_until_parked();
19857 cx.assert_state_with_diff(
19858 r#"
19859 use some::mod1;
19860 use some::mod2;
19861
19862 - const A: u32 = 42;
19863 - const B: u32 = 42;
19864 - const C: u32 = 42;
19865 ˇ
19866
19867 fn main() {
19868 println!("hello");
19869
19870 println!("world");
19871 }
19872 "#
19873 .unindent(),
19874 );
19875
19876 cx.update_editor(|editor, window, cx| {
19877 editor.handle_input("replacement", window, cx);
19878 });
19879 executor.run_until_parked();
19880 cx.assert_state_with_diff(
19881 r#"
19882 use some::mod1;
19883 use some::mod2;
19884
19885 - const A: u32 = 42;
19886 - const B: u32 = 42;
19887 - const C: u32 = 42;
19888 -
19889 + replacementˇ
19890
19891 fn main() {
19892 println!("hello");
19893
19894 println!("world");
19895 }
19896 "#
19897 .unindent(),
19898 );
19899}
19900
19901#[gpui::test]
19902async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19903 init_test(cx, |_| {});
19904
19905 let mut cx = EditorTestContext::new(cx).await;
19906
19907 let base_text = r#"
19908 one
19909 two
19910 three
19911 four
19912 five
19913 "#
19914 .unindent();
19915 executor.run_until_parked();
19916 cx.set_state(
19917 &r#"
19918 one
19919 two
19920 fˇour
19921 five
19922 "#
19923 .unindent(),
19924 );
19925
19926 cx.set_head_text(&base_text);
19927 executor.run_until_parked();
19928
19929 cx.update_editor(|editor, window, cx| {
19930 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19931 });
19932 executor.run_until_parked();
19933
19934 cx.assert_state_with_diff(
19935 r#"
19936 one
19937 two
19938 - three
19939 fˇour
19940 five
19941 "#
19942 .unindent(),
19943 );
19944
19945 cx.update_editor(|editor, window, cx| {
19946 editor.backspace(&Backspace, window, cx);
19947 editor.backspace(&Backspace, window, cx);
19948 });
19949 executor.run_until_parked();
19950 cx.assert_state_with_diff(
19951 r#"
19952 one
19953 two
19954 - threeˇ
19955 - four
19956 + our
19957 five
19958 "#
19959 .unindent(),
19960 );
19961}
19962
19963#[gpui::test]
19964async fn test_edit_after_expanded_modification_hunk(
19965 executor: BackgroundExecutor,
19966 cx: &mut TestAppContext,
19967) {
19968 init_test(cx, |_| {});
19969
19970 let mut cx = EditorTestContext::new(cx).await;
19971
19972 let diff_base = r#"
19973 use some::mod1;
19974 use some::mod2;
19975
19976 const A: u32 = 42;
19977 const B: u32 = 42;
19978 const C: u32 = 42;
19979 const D: u32 = 42;
19980
19981
19982 fn main() {
19983 println!("hello");
19984
19985 println!("world");
19986 }"#
19987 .unindent();
19988
19989 cx.set_state(
19990 &r#"
19991 use some::mod1;
19992 use some::mod2;
19993
19994 const A: u32 = 42;
19995 const B: u32 = 42;
19996 const C: u32 = 43ˇ
19997 const D: u32 = 42;
19998
19999
20000 fn main() {
20001 println!("hello");
20002
20003 println!("world");
20004 }"#
20005 .unindent(),
20006 );
20007
20008 cx.set_head_text(&diff_base);
20009 executor.run_until_parked();
20010 cx.update_editor(|editor, window, cx| {
20011 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20012 });
20013 executor.run_until_parked();
20014
20015 cx.assert_state_with_diff(
20016 r#"
20017 use some::mod1;
20018 use some::mod2;
20019
20020 const A: u32 = 42;
20021 const B: u32 = 42;
20022 - const C: u32 = 42;
20023 + const C: u32 = 43ˇ
20024 const D: u32 = 42;
20025
20026
20027 fn main() {
20028 println!("hello");
20029
20030 println!("world");
20031 }"#
20032 .unindent(),
20033 );
20034
20035 cx.update_editor(|editor, window, cx| {
20036 editor.handle_input("\nnew_line\n", window, cx);
20037 });
20038 executor.run_until_parked();
20039
20040 cx.assert_state_with_diff(
20041 r#"
20042 use some::mod1;
20043 use some::mod2;
20044
20045 const A: u32 = 42;
20046 const B: u32 = 42;
20047 - const C: u32 = 42;
20048 + const C: u32 = 43
20049 + new_line
20050 + ˇ
20051 const D: u32 = 42;
20052
20053
20054 fn main() {
20055 println!("hello");
20056
20057 println!("world");
20058 }"#
20059 .unindent(),
20060 );
20061}
20062
20063#[gpui::test]
20064async fn test_stage_and_unstage_added_file_hunk(
20065 executor: BackgroundExecutor,
20066 cx: &mut TestAppContext,
20067) {
20068 init_test(cx, |_| {});
20069
20070 let mut cx = EditorTestContext::new(cx).await;
20071 cx.update_editor(|editor, _, cx| {
20072 editor.set_expand_all_diff_hunks(cx);
20073 });
20074
20075 let working_copy = r#"
20076 ˇfn main() {
20077 println!("hello, world!");
20078 }
20079 "#
20080 .unindent();
20081
20082 cx.set_state(&working_copy);
20083 executor.run_until_parked();
20084
20085 cx.assert_state_with_diff(
20086 r#"
20087 + ˇfn main() {
20088 + println!("hello, world!");
20089 + }
20090 "#
20091 .unindent(),
20092 );
20093 cx.assert_index_text(None);
20094
20095 cx.update_editor(|editor, window, cx| {
20096 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20097 });
20098 executor.run_until_parked();
20099 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20100 cx.assert_state_with_diff(
20101 r#"
20102 + ˇfn main() {
20103 + println!("hello, world!");
20104 + }
20105 "#
20106 .unindent(),
20107 );
20108
20109 cx.update_editor(|editor, window, cx| {
20110 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20111 });
20112 executor.run_until_parked();
20113 cx.assert_index_text(None);
20114}
20115
20116async fn setup_indent_guides_editor(
20117 text: &str,
20118 cx: &mut TestAppContext,
20119) -> (BufferId, EditorTestContext) {
20120 init_test(cx, |_| {});
20121
20122 let mut cx = EditorTestContext::new(cx).await;
20123
20124 let buffer_id = cx.update_editor(|editor, window, cx| {
20125 editor.set_text(text, window, cx);
20126 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20127
20128 buffer_ids[0]
20129 });
20130
20131 (buffer_id, cx)
20132}
20133
20134fn assert_indent_guides(
20135 range: Range<u32>,
20136 expected: Vec<IndentGuide>,
20137 active_indices: Option<Vec<usize>>,
20138 cx: &mut EditorTestContext,
20139) {
20140 let indent_guides = cx.update_editor(|editor, window, cx| {
20141 let snapshot = editor.snapshot(window, cx).display_snapshot;
20142 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20143 editor,
20144 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20145 true,
20146 &snapshot,
20147 cx,
20148 );
20149
20150 indent_guides.sort_by(|a, b| {
20151 a.depth.cmp(&b.depth).then(
20152 a.start_row
20153 .cmp(&b.start_row)
20154 .then(a.end_row.cmp(&b.end_row)),
20155 )
20156 });
20157 indent_guides
20158 });
20159
20160 if let Some(expected) = active_indices {
20161 let active_indices = cx.update_editor(|editor, window, cx| {
20162 let snapshot = editor.snapshot(window, cx).display_snapshot;
20163 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20164 });
20165
20166 assert_eq!(
20167 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20168 expected,
20169 "Active indent guide indices do not match"
20170 );
20171 }
20172
20173 assert_eq!(indent_guides, expected, "Indent guides do not match");
20174}
20175
20176fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20177 IndentGuide {
20178 buffer_id,
20179 start_row: MultiBufferRow(start_row),
20180 end_row: MultiBufferRow(end_row),
20181 depth,
20182 tab_size: 4,
20183 settings: IndentGuideSettings {
20184 enabled: true,
20185 line_width: 1,
20186 active_line_width: 1,
20187 coloring: IndentGuideColoring::default(),
20188 background_coloring: IndentGuideBackgroundColoring::default(),
20189 },
20190 }
20191}
20192
20193#[gpui::test]
20194async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20195 let (buffer_id, mut cx) = setup_indent_guides_editor(
20196 &"
20197 fn main() {
20198 let a = 1;
20199 }"
20200 .unindent(),
20201 cx,
20202 )
20203 .await;
20204
20205 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20206}
20207
20208#[gpui::test]
20209async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20210 let (buffer_id, mut cx) = setup_indent_guides_editor(
20211 &"
20212 fn main() {
20213 let a = 1;
20214 let b = 2;
20215 }"
20216 .unindent(),
20217 cx,
20218 )
20219 .await;
20220
20221 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20222}
20223
20224#[gpui::test]
20225async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20226 let (buffer_id, mut cx) = setup_indent_guides_editor(
20227 &"
20228 fn main() {
20229 let a = 1;
20230 if a == 3 {
20231 let b = 2;
20232 } else {
20233 let c = 3;
20234 }
20235 }"
20236 .unindent(),
20237 cx,
20238 )
20239 .await;
20240
20241 assert_indent_guides(
20242 0..8,
20243 vec![
20244 indent_guide(buffer_id, 1, 6, 0),
20245 indent_guide(buffer_id, 3, 3, 1),
20246 indent_guide(buffer_id, 5, 5, 1),
20247 ],
20248 None,
20249 &mut cx,
20250 );
20251}
20252
20253#[gpui::test]
20254async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20255 let (buffer_id, mut cx) = setup_indent_guides_editor(
20256 &"
20257 fn main() {
20258 let a = 1;
20259 let b = 2;
20260 let c = 3;
20261 }"
20262 .unindent(),
20263 cx,
20264 )
20265 .await;
20266
20267 assert_indent_guides(
20268 0..5,
20269 vec![
20270 indent_guide(buffer_id, 1, 3, 0),
20271 indent_guide(buffer_id, 2, 2, 1),
20272 ],
20273 None,
20274 &mut cx,
20275 );
20276}
20277
20278#[gpui::test]
20279async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20280 let (buffer_id, mut cx) = setup_indent_guides_editor(
20281 &"
20282 fn main() {
20283 let a = 1;
20284
20285 let c = 3;
20286 }"
20287 .unindent(),
20288 cx,
20289 )
20290 .await;
20291
20292 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20293}
20294
20295#[gpui::test]
20296async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20297 let (buffer_id, mut cx) = setup_indent_guides_editor(
20298 &"
20299 fn main() {
20300 let a = 1;
20301
20302 let c = 3;
20303
20304 if a == 3 {
20305 let b = 2;
20306 } else {
20307 let c = 3;
20308 }
20309 }"
20310 .unindent(),
20311 cx,
20312 )
20313 .await;
20314
20315 assert_indent_guides(
20316 0..11,
20317 vec![
20318 indent_guide(buffer_id, 1, 9, 0),
20319 indent_guide(buffer_id, 6, 6, 1),
20320 indent_guide(buffer_id, 8, 8, 1),
20321 ],
20322 None,
20323 &mut cx,
20324 );
20325}
20326
20327#[gpui::test]
20328async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20329 let (buffer_id, mut cx) = setup_indent_guides_editor(
20330 &"
20331 fn main() {
20332 let a = 1;
20333
20334 let c = 3;
20335
20336 if a == 3 {
20337 let b = 2;
20338 } else {
20339 let c = 3;
20340 }
20341 }"
20342 .unindent(),
20343 cx,
20344 )
20345 .await;
20346
20347 assert_indent_guides(
20348 1..11,
20349 vec![
20350 indent_guide(buffer_id, 1, 9, 0),
20351 indent_guide(buffer_id, 6, 6, 1),
20352 indent_guide(buffer_id, 8, 8, 1),
20353 ],
20354 None,
20355 &mut cx,
20356 );
20357}
20358
20359#[gpui::test]
20360async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20361 let (buffer_id, mut cx) = setup_indent_guides_editor(
20362 &"
20363 fn main() {
20364 let a = 1;
20365
20366 let c = 3;
20367
20368 if a == 3 {
20369 let b = 2;
20370 } else {
20371 let c = 3;
20372 }
20373 }"
20374 .unindent(),
20375 cx,
20376 )
20377 .await;
20378
20379 assert_indent_guides(
20380 1..10,
20381 vec![
20382 indent_guide(buffer_id, 1, 9, 0),
20383 indent_guide(buffer_id, 6, 6, 1),
20384 indent_guide(buffer_id, 8, 8, 1),
20385 ],
20386 None,
20387 &mut cx,
20388 );
20389}
20390
20391#[gpui::test]
20392async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20393 let (buffer_id, mut cx) = setup_indent_guides_editor(
20394 &"
20395 fn main() {
20396 if a {
20397 b(
20398 c,
20399 d,
20400 )
20401 } else {
20402 e(
20403 f
20404 )
20405 }
20406 }"
20407 .unindent(),
20408 cx,
20409 )
20410 .await;
20411
20412 assert_indent_guides(
20413 0..11,
20414 vec![
20415 indent_guide(buffer_id, 1, 10, 0),
20416 indent_guide(buffer_id, 2, 5, 1),
20417 indent_guide(buffer_id, 7, 9, 1),
20418 indent_guide(buffer_id, 3, 4, 2),
20419 indent_guide(buffer_id, 8, 8, 2),
20420 ],
20421 None,
20422 &mut cx,
20423 );
20424
20425 cx.update_editor(|editor, window, cx| {
20426 editor.fold_at(MultiBufferRow(2), window, cx);
20427 assert_eq!(
20428 editor.display_text(cx),
20429 "
20430 fn main() {
20431 if a {
20432 b(⋯
20433 )
20434 } else {
20435 e(
20436 f
20437 )
20438 }
20439 }"
20440 .unindent()
20441 );
20442 });
20443
20444 assert_indent_guides(
20445 0..11,
20446 vec![
20447 indent_guide(buffer_id, 1, 10, 0),
20448 indent_guide(buffer_id, 2, 5, 1),
20449 indent_guide(buffer_id, 7, 9, 1),
20450 indent_guide(buffer_id, 8, 8, 2),
20451 ],
20452 None,
20453 &mut cx,
20454 );
20455}
20456
20457#[gpui::test]
20458async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20459 let (buffer_id, mut cx) = setup_indent_guides_editor(
20460 &"
20461 block1
20462 block2
20463 block3
20464 block4
20465 block2
20466 block1
20467 block1"
20468 .unindent(),
20469 cx,
20470 )
20471 .await;
20472
20473 assert_indent_guides(
20474 1..10,
20475 vec![
20476 indent_guide(buffer_id, 1, 4, 0),
20477 indent_guide(buffer_id, 2, 3, 1),
20478 indent_guide(buffer_id, 3, 3, 2),
20479 ],
20480 None,
20481 &mut cx,
20482 );
20483}
20484
20485#[gpui::test]
20486async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20487 let (buffer_id, mut cx) = setup_indent_guides_editor(
20488 &"
20489 block1
20490 block2
20491 block3
20492
20493 block1
20494 block1"
20495 .unindent(),
20496 cx,
20497 )
20498 .await;
20499
20500 assert_indent_guides(
20501 0..6,
20502 vec![
20503 indent_guide(buffer_id, 1, 2, 0),
20504 indent_guide(buffer_id, 2, 2, 1),
20505 ],
20506 None,
20507 &mut cx,
20508 );
20509}
20510
20511#[gpui::test]
20512async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20513 let (buffer_id, mut cx) = setup_indent_guides_editor(
20514 &"
20515 function component() {
20516 \treturn (
20517 \t\t\t
20518 \t\t<div>
20519 \t\t\t<abc></abc>
20520 \t\t</div>
20521 \t)
20522 }"
20523 .unindent(),
20524 cx,
20525 )
20526 .await;
20527
20528 assert_indent_guides(
20529 0..8,
20530 vec![
20531 indent_guide(buffer_id, 1, 6, 0),
20532 indent_guide(buffer_id, 2, 5, 1),
20533 indent_guide(buffer_id, 4, 4, 2),
20534 ],
20535 None,
20536 &mut cx,
20537 );
20538}
20539
20540#[gpui::test]
20541async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20542 let (buffer_id, mut cx) = setup_indent_guides_editor(
20543 &"
20544 function component() {
20545 \treturn (
20546 \t
20547 \t\t<div>
20548 \t\t\t<abc></abc>
20549 \t\t</div>
20550 \t)
20551 }"
20552 .unindent(),
20553 cx,
20554 )
20555 .await;
20556
20557 assert_indent_guides(
20558 0..8,
20559 vec![
20560 indent_guide(buffer_id, 1, 6, 0),
20561 indent_guide(buffer_id, 2, 5, 1),
20562 indent_guide(buffer_id, 4, 4, 2),
20563 ],
20564 None,
20565 &mut cx,
20566 );
20567}
20568
20569#[gpui::test]
20570async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20571 let (buffer_id, mut cx) = setup_indent_guides_editor(
20572 &"
20573 block1
20574
20575
20576
20577 block2
20578 "
20579 .unindent(),
20580 cx,
20581 )
20582 .await;
20583
20584 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20585}
20586
20587#[gpui::test]
20588async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20589 let (buffer_id, mut cx) = setup_indent_guides_editor(
20590 &"
20591 def a:
20592 \tb = 3
20593 \tif True:
20594 \t\tc = 4
20595 \t\td = 5
20596 \tprint(b)
20597 "
20598 .unindent(),
20599 cx,
20600 )
20601 .await;
20602
20603 assert_indent_guides(
20604 0..6,
20605 vec![
20606 indent_guide(buffer_id, 1, 5, 0),
20607 indent_guide(buffer_id, 3, 4, 1),
20608 ],
20609 None,
20610 &mut cx,
20611 );
20612}
20613
20614#[gpui::test]
20615async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20616 let (buffer_id, mut cx) = setup_indent_guides_editor(
20617 &"
20618 fn main() {
20619 let a = 1;
20620 }"
20621 .unindent(),
20622 cx,
20623 )
20624 .await;
20625
20626 cx.update_editor(|editor, window, cx| {
20627 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20628 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20629 });
20630 });
20631
20632 assert_indent_guides(
20633 0..3,
20634 vec![indent_guide(buffer_id, 1, 1, 0)],
20635 Some(vec![0]),
20636 &mut cx,
20637 );
20638}
20639
20640#[gpui::test]
20641async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20642 let (buffer_id, mut cx) = setup_indent_guides_editor(
20643 &"
20644 fn main() {
20645 if 1 == 2 {
20646 let a = 1;
20647 }
20648 }"
20649 .unindent(),
20650 cx,
20651 )
20652 .await;
20653
20654 cx.update_editor(|editor, window, cx| {
20655 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20656 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20657 });
20658 });
20659
20660 assert_indent_guides(
20661 0..4,
20662 vec![
20663 indent_guide(buffer_id, 1, 3, 0),
20664 indent_guide(buffer_id, 2, 2, 1),
20665 ],
20666 Some(vec![1]),
20667 &mut cx,
20668 );
20669
20670 cx.update_editor(|editor, window, cx| {
20671 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20672 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20673 });
20674 });
20675
20676 assert_indent_guides(
20677 0..4,
20678 vec![
20679 indent_guide(buffer_id, 1, 3, 0),
20680 indent_guide(buffer_id, 2, 2, 1),
20681 ],
20682 Some(vec![1]),
20683 &mut cx,
20684 );
20685
20686 cx.update_editor(|editor, window, cx| {
20687 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20688 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20689 });
20690 });
20691
20692 assert_indent_guides(
20693 0..4,
20694 vec![
20695 indent_guide(buffer_id, 1, 3, 0),
20696 indent_guide(buffer_id, 2, 2, 1),
20697 ],
20698 Some(vec![0]),
20699 &mut cx,
20700 );
20701}
20702
20703#[gpui::test]
20704async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20705 let (buffer_id, mut cx) = setup_indent_guides_editor(
20706 &"
20707 fn main() {
20708 let a = 1;
20709
20710 let b = 2;
20711 }"
20712 .unindent(),
20713 cx,
20714 )
20715 .await;
20716
20717 cx.update_editor(|editor, window, cx| {
20718 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20719 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20720 });
20721 });
20722
20723 assert_indent_guides(
20724 0..5,
20725 vec![indent_guide(buffer_id, 1, 3, 0)],
20726 Some(vec![0]),
20727 &mut cx,
20728 );
20729}
20730
20731#[gpui::test]
20732async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20733 let (buffer_id, mut cx) = setup_indent_guides_editor(
20734 &"
20735 def m:
20736 a = 1
20737 pass"
20738 .unindent(),
20739 cx,
20740 )
20741 .await;
20742
20743 cx.update_editor(|editor, window, cx| {
20744 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20745 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20746 });
20747 });
20748
20749 assert_indent_guides(
20750 0..3,
20751 vec![indent_guide(buffer_id, 1, 2, 0)],
20752 Some(vec![0]),
20753 &mut cx,
20754 );
20755}
20756
20757#[gpui::test]
20758async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20759 init_test(cx, |_| {});
20760 let mut cx = EditorTestContext::new(cx).await;
20761 let text = indoc! {
20762 "
20763 impl A {
20764 fn b() {
20765 0;
20766 3;
20767 5;
20768 6;
20769 7;
20770 }
20771 }
20772 "
20773 };
20774 let base_text = indoc! {
20775 "
20776 impl A {
20777 fn b() {
20778 0;
20779 1;
20780 2;
20781 3;
20782 4;
20783 }
20784 fn c() {
20785 5;
20786 6;
20787 7;
20788 }
20789 }
20790 "
20791 };
20792
20793 cx.update_editor(|editor, window, cx| {
20794 editor.set_text(text, window, cx);
20795
20796 editor.buffer().update(cx, |multibuffer, cx| {
20797 let buffer = multibuffer.as_singleton().unwrap();
20798 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20799
20800 multibuffer.set_all_diff_hunks_expanded(cx);
20801 multibuffer.add_diff(diff, cx);
20802
20803 buffer.read(cx).remote_id()
20804 })
20805 });
20806 cx.run_until_parked();
20807
20808 cx.assert_state_with_diff(
20809 indoc! { "
20810 impl A {
20811 fn b() {
20812 0;
20813 - 1;
20814 - 2;
20815 3;
20816 - 4;
20817 - }
20818 - fn c() {
20819 5;
20820 6;
20821 7;
20822 }
20823 }
20824 ˇ"
20825 }
20826 .to_string(),
20827 );
20828
20829 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20830 editor
20831 .snapshot(window, cx)
20832 .buffer_snapshot()
20833 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20834 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20835 .collect::<Vec<_>>()
20836 });
20837 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20838 assert_eq!(
20839 actual_guides,
20840 vec![
20841 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20842 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20843 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20844 ]
20845 );
20846}
20847
20848#[gpui::test]
20849async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20850 init_test(cx, |_| {});
20851 let mut cx = EditorTestContext::new(cx).await;
20852
20853 let diff_base = r#"
20854 a
20855 b
20856 c
20857 "#
20858 .unindent();
20859
20860 cx.set_state(
20861 &r#"
20862 ˇA
20863 b
20864 C
20865 "#
20866 .unindent(),
20867 );
20868 cx.set_head_text(&diff_base);
20869 cx.update_editor(|editor, window, cx| {
20870 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20871 });
20872 executor.run_until_parked();
20873
20874 let both_hunks_expanded = r#"
20875 - a
20876 + ˇA
20877 b
20878 - c
20879 + C
20880 "#
20881 .unindent();
20882
20883 cx.assert_state_with_diff(both_hunks_expanded.clone());
20884
20885 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20886 let snapshot = editor.snapshot(window, cx);
20887 let hunks = editor
20888 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
20889 .collect::<Vec<_>>();
20890 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20891 let buffer_id = hunks[0].buffer_id;
20892 hunks
20893 .into_iter()
20894 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20895 .collect::<Vec<_>>()
20896 });
20897 assert_eq!(hunk_ranges.len(), 2);
20898
20899 cx.update_editor(|editor, _, cx| {
20900 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20901 });
20902 executor.run_until_parked();
20903
20904 let second_hunk_expanded = r#"
20905 ˇA
20906 b
20907 - c
20908 + C
20909 "#
20910 .unindent();
20911
20912 cx.assert_state_with_diff(second_hunk_expanded);
20913
20914 cx.update_editor(|editor, _, cx| {
20915 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20916 });
20917 executor.run_until_parked();
20918
20919 cx.assert_state_with_diff(both_hunks_expanded.clone());
20920
20921 cx.update_editor(|editor, _, cx| {
20922 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20923 });
20924 executor.run_until_parked();
20925
20926 let first_hunk_expanded = r#"
20927 - a
20928 + ˇA
20929 b
20930 C
20931 "#
20932 .unindent();
20933
20934 cx.assert_state_with_diff(first_hunk_expanded);
20935
20936 cx.update_editor(|editor, _, cx| {
20937 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20938 });
20939 executor.run_until_parked();
20940
20941 cx.assert_state_with_diff(both_hunks_expanded);
20942
20943 cx.set_state(
20944 &r#"
20945 ˇA
20946 b
20947 "#
20948 .unindent(),
20949 );
20950 cx.run_until_parked();
20951
20952 // TODO this cursor position seems bad
20953 cx.assert_state_with_diff(
20954 r#"
20955 - ˇa
20956 + A
20957 b
20958 "#
20959 .unindent(),
20960 );
20961
20962 cx.update_editor(|editor, window, cx| {
20963 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20964 });
20965
20966 cx.assert_state_with_diff(
20967 r#"
20968 - ˇa
20969 + A
20970 b
20971 - c
20972 "#
20973 .unindent(),
20974 );
20975
20976 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20977 let snapshot = editor.snapshot(window, cx);
20978 let hunks = editor
20979 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
20980 .collect::<Vec<_>>();
20981 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20982 let buffer_id = hunks[0].buffer_id;
20983 hunks
20984 .into_iter()
20985 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20986 .collect::<Vec<_>>()
20987 });
20988 assert_eq!(hunk_ranges.len(), 2);
20989
20990 cx.update_editor(|editor, _, cx| {
20991 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20992 });
20993 executor.run_until_parked();
20994
20995 cx.assert_state_with_diff(
20996 r#"
20997 - ˇa
20998 + A
20999 b
21000 "#
21001 .unindent(),
21002 );
21003}
21004
21005#[gpui::test]
21006async fn test_toggle_deletion_hunk_at_start_of_file(
21007 executor: BackgroundExecutor,
21008 cx: &mut TestAppContext,
21009) {
21010 init_test(cx, |_| {});
21011 let mut cx = EditorTestContext::new(cx).await;
21012
21013 let diff_base = r#"
21014 a
21015 b
21016 c
21017 "#
21018 .unindent();
21019
21020 cx.set_state(
21021 &r#"
21022 ˇb
21023 c
21024 "#
21025 .unindent(),
21026 );
21027 cx.set_head_text(&diff_base);
21028 cx.update_editor(|editor, window, cx| {
21029 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21030 });
21031 executor.run_until_parked();
21032
21033 let hunk_expanded = r#"
21034 - a
21035 ˇb
21036 c
21037 "#
21038 .unindent();
21039
21040 cx.assert_state_with_diff(hunk_expanded.clone());
21041
21042 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21043 let snapshot = editor.snapshot(window, cx);
21044 let hunks = editor
21045 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21046 .collect::<Vec<_>>();
21047 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21048 let buffer_id = hunks[0].buffer_id;
21049 hunks
21050 .into_iter()
21051 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21052 .collect::<Vec<_>>()
21053 });
21054 assert_eq!(hunk_ranges.len(), 1);
21055
21056 cx.update_editor(|editor, _, cx| {
21057 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21058 });
21059 executor.run_until_parked();
21060
21061 let hunk_collapsed = r#"
21062 ˇb
21063 c
21064 "#
21065 .unindent();
21066
21067 cx.assert_state_with_diff(hunk_collapsed);
21068
21069 cx.update_editor(|editor, _, cx| {
21070 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21071 });
21072 executor.run_until_parked();
21073
21074 cx.assert_state_with_diff(hunk_expanded);
21075}
21076
21077#[gpui::test]
21078async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21079 init_test(cx, |_| {});
21080
21081 let fs = FakeFs::new(cx.executor());
21082 fs.insert_tree(
21083 path!("/test"),
21084 json!({
21085 ".git": {},
21086 "file-1": "ONE\n",
21087 "file-2": "TWO\n",
21088 "file-3": "THREE\n",
21089 }),
21090 )
21091 .await;
21092
21093 fs.set_head_for_repo(
21094 path!("/test/.git").as_ref(),
21095 &[
21096 ("file-1", "one\n".into()),
21097 ("file-2", "two\n".into()),
21098 ("file-3", "three\n".into()),
21099 ],
21100 "deadbeef",
21101 );
21102
21103 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21104 let mut buffers = vec![];
21105 for i in 1..=3 {
21106 let buffer = project
21107 .update(cx, |project, cx| {
21108 let path = format!(path!("/test/file-{}"), i);
21109 project.open_local_buffer(path, cx)
21110 })
21111 .await
21112 .unwrap();
21113 buffers.push(buffer);
21114 }
21115
21116 let multibuffer = cx.new(|cx| {
21117 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21118 multibuffer.set_all_diff_hunks_expanded(cx);
21119 for buffer in &buffers {
21120 let snapshot = buffer.read(cx).snapshot();
21121 multibuffer.set_excerpts_for_path(
21122 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21123 buffer.clone(),
21124 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21125 2,
21126 cx,
21127 );
21128 }
21129 multibuffer
21130 });
21131
21132 let editor = cx.add_window(|window, cx| {
21133 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21134 });
21135 cx.run_until_parked();
21136
21137 let snapshot = editor
21138 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21139 .unwrap();
21140 let hunks = snapshot
21141 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21142 .map(|hunk| match hunk {
21143 DisplayDiffHunk::Unfolded {
21144 display_row_range, ..
21145 } => display_row_range,
21146 DisplayDiffHunk::Folded { .. } => unreachable!(),
21147 })
21148 .collect::<Vec<_>>();
21149 assert_eq!(
21150 hunks,
21151 [
21152 DisplayRow(2)..DisplayRow(4),
21153 DisplayRow(7)..DisplayRow(9),
21154 DisplayRow(12)..DisplayRow(14),
21155 ]
21156 );
21157}
21158
21159#[gpui::test]
21160async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21161 init_test(cx, |_| {});
21162
21163 let mut cx = EditorTestContext::new(cx).await;
21164 cx.set_head_text(indoc! { "
21165 one
21166 two
21167 three
21168 four
21169 five
21170 "
21171 });
21172 cx.set_index_text(indoc! { "
21173 one
21174 two
21175 three
21176 four
21177 five
21178 "
21179 });
21180 cx.set_state(indoc! {"
21181 one
21182 TWO
21183 ˇTHREE
21184 FOUR
21185 five
21186 "});
21187 cx.run_until_parked();
21188 cx.update_editor(|editor, window, cx| {
21189 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21190 });
21191 cx.run_until_parked();
21192 cx.assert_index_text(Some(indoc! {"
21193 one
21194 TWO
21195 THREE
21196 FOUR
21197 five
21198 "}));
21199 cx.set_state(indoc! { "
21200 one
21201 TWO
21202 ˇTHREE-HUNDRED
21203 FOUR
21204 five
21205 "});
21206 cx.run_until_parked();
21207 cx.update_editor(|editor, window, cx| {
21208 let snapshot = editor.snapshot(window, cx);
21209 let hunks = editor
21210 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21211 .collect::<Vec<_>>();
21212 assert_eq!(hunks.len(), 1);
21213 assert_eq!(
21214 hunks[0].status(),
21215 DiffHunkStatus {
21216 kind: DiffHunkStatusKind::Modified,
21217 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21218 }
21219 );
21220
21221 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21222 });
21223 cx.run_until_parked();
21224 cx.assert_index_text(Some(indoc! {"
21225 one
21226 TWO
21227 THREE-HUNDRED
21228 FOUR
21229 five
21230 "}));
21231}
21232
21233#[gpui::test]
21234fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21235 init_test(cx, |_| {});
21236
21237 let editor = cx.add_window(|window, cx| {
21238 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21239 build_editor(buffer, window, cx)
21240 });
21241
21242 let render_args = Arc::new(Mutex::new(None));
21243 let snapshot = editor
21244 .update(cx, |editor, window, cx| {
21245 let snapshot = editor.buffer().read(cx).snapshot(cx);
21246 let range =
21247 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21248
21249 struct RenderArgs {
21250 row: MultiBufferRow,
21251 folded: bool,
21252 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21253 }
21254
21255 let crease = Crease::inline(
21256 range,
21257 FoldPlaceholder::test(),
21258 {
21259 let toggle_callback = render_args.clone();
21260 move |row, folded, callback, _window, _cx| {
21261 *toggle_callback.lock() = Some(RenderArgs {
21262 row,
21263 folded,
21264 callback,
21265 });
21266 div()
21267 }
21268 },
21269 |_row, _folded, _window, _cx| div(),
21270 );
21271
21272 editor.insert_creases(Some(crease), cx);
21273 let snapshot = editor.snapshot(window, cx);
21274 let _div =
21275 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21276 snapshot
21277 })
21278 .unwrap();
21279
21280 let render_args = render_args.lock().take().unwrap();
21281 assert_eq!(render_args.row, MultiBufferRow(1));
21282 assert!(!render_args.folded);
21283 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21284
21285 cx.update_window(*editor, |_, window, cx| {
21286 (render_args.callback)(true, window, cx)
21287 })
21288 .unwrap();
21289 let snapshot = editor
21290 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21291 .unwrap();
21292 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21293
21294 cx.update_window(*editor, |_, window, cx| {
21295 (render_args.callback)(false, window, cx)
21296 })
21297 .unwrap();
21298 let snapshot = editor
21299 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21300 .unwrap();
21301 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21302}
21303
21304#[gpui::test]
21305async fn test_input_text(cx: &mut TestAppContext) {
21306 init_test(cx, |_| {});
21307 let mut cx = EditorTestContext::new(cx).await;
21308
21309 cx.set_state(
21310 &r#"ˇone
21311 two
21312
21313 three
21314 fourˇ
21315 five
21316
21317 siˇx"#
21318 .unindent(),
21319 );
21320
21321 cx.dispatch_action(HandleInput(String::new()));
21322 cx.assert_editor_state(
21323 &r#"ˇone
21324 two
21325
21326 three
21327 fourˇ
21328 five
21329
21330 siˇx"#
21331 .unindent(),
21332 );
21333
21334 cx.dispatch_action(HandleInput("AAAA".to_string()));
21335 cx.assert_editor_state(
21336 &r#"AAAAˇone
21337 two
21338
21339 three
21340 fourAAAAˇ
21341 five
21342
21343 siAAAAˇx"#
21344 .unindent(),
21345 );
21346}
21347
21348#[gpui::test]
21349async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21350 init_test(cx, |_| {});
21351
21352 let mut cx = EditorTestContext::new(cx).await;
21353 cx.set_state(
21354 r#"let foo = 1;
21355let foo = 2;
21356let foo = 3;
21357let fooˇ = 4;
21358let foo = 5;
21359let foo = 6;
21360let foo = 7;
21361let foo = 8;
21362let foo = 9;
21363let foo = 10;
21364let foo = 11;
21365let foo = 12;
21366let foo = 13;
21367let foo = 14;
21368let foo = 15;"#,
21369 );
21370
21371 cx.update_editor(|e, window, cx| {
21372 assert_eq!(
21373 e.next_scroll_position,
21374 NextScrollCursorCenterTopBottom::Center,
21375 "Default next scroll direction is center",
21376 );
21377
21378 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21379 assert_eq!(
21380 e.next_scroll_position,
21381 NextScrollCursorCenterTopBottom::Top,
21382 "After center, next scroll direction should be top",
21383 );
21384
21385 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21386 assert_eq!(
21387 e.next_scroll_position,
21388 NextScrollCursorCenterTopBottom::Bottom,
21389 "After top, next scroll direction should be bottom",
21390 );
21391
21392 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21393 assert_eq!(
21394 e.next_scroll_position,
21395 NextScrollCursorCenterTopBottom::Center,
21396 "After bottom, scrolling should start over",
21397 );
21398
21399 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21400 assert_eq!(
21401 e.next_scroll_position,
21402 NextScrollCursorCenterTopBottom::Top,
21403 "Scrolling continues if retriggered fast enough"
21404 );
21405 });
21406
21407 cx.executor()
21408 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21409 cx.executor().run_until_parked();
21410 cx.update_editor(|e, _, _| {
21411 assert_eq!(
21412 e.next_scroll_position,
21413 NextScrollCursorCenterTopBottom::Center,
21414 "If scrolling is not triggered fast enough, it should reset"
21415 );
21416 });
21417}
21418
21419#[gpui::test]
21420async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21421 init_test(cx, |_| {});
21422 let mut cx = EditorLspTestContext::new_rust(
21423 lsp::ServerCapabilities {
21424 definition_provider: Some(lsp::OneOf::Left(true)),
21425 references_provider: Some(lsp::OneOf::Left(true)),
21426 ..lsp::ServerCapabilities::default()
21427 },
21428 cx,
21429 )
21430 .await;
21431
21432 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21433 let go_to_definition = cx
21434 .lsp
21435 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21436 move |params, _| async move {
21437 if empty_go_to_definition {
21438 Ok(None)
21439 } else {
21440 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21441 uri: params.text_document_position_params.text_document.uri,
21442 range: lsp::Range::new(
21443 lsp::Position::new(4, 3),
21444 lsp::Position::new(4, 6),
21445 ),
21446 })))
21447 }
21448 },
21449 );
21450 let references = cx
21451 .lsp
21452 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21453 Ok(Some(vec![lsp::Location {
21454 uri: params.text_document_position.text_document.uri,
21455 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21456 }]))
21457 });
21458 (go_to_definition, references)
21459 };
21460
21461 cx.set_state(
21462 &r#"fn one() {
21463 let mut a = ˇtwo();
21464 }
21465
21466 fn two() {}"#
21467 .unindent(),
21468 );
21469 set_up_lsp_handlers(false, &mut cx);
21470 let navigated = cx
21471 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21472 .await
21473 .expect("Failed to navigate to definition");
21474 assert_eq!(
21475 navigated,
21476 Navigated::Yes,
21477 "Should have navigated to definition from the GetDefinition response"
21478 );
21479 cx.assert_editor_state(
21480 &r#"fn one() {
21481 let mut a = two();
21482 }
21483
21484 fn «twoˇ»() {}"#
21485 .unindent(),
21486 );
21487
21488 let editors = cx.update_workspace(|workspace, _, cx| {
21489 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21490 });
21491 cx.update_editor(|_, _, test_editor_cx| {
21492 assert_eq!(
21493 editors.len(),
21494 1,
21495 "Initially, only one, test, editor should be open in the workspace"
21496 );
21497 assert_eq!(
21498 test_editor_cx.entity(),
21499 editors.last().expect("Asserted len is 1").clone()
21500 );
21501 });
21502
21503 set_up_lsp_handlers(true, &mut cx);
21504 let navigated = cx
21505 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21506 .await
21507 .expect("Failed to navigate to lookup references");
21508 assert_eq!(
21509 navigated,
21510 Navigated::Yes,
21511 "Should have navigated to references as a fallback after empty GoToDefinition response"
21512 );
21513 // We should not change the selections in the existing file,
21514 // if opening another milti buffer with the references
21515 cx.assert_editor_state(
21516 &r#"fn one() {
21517 let mut a = two();
21518 }
21519
21520 fn «twoˇ»() {}"#
21521 .unindent(),
21522 );
21523 let editors = cx.update_workspace(|workspace, _, cx| {
21524 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21525 });
21526 cx.update_editor(|_, _, test_editor_cx| {
21527 assert_eq!(
21528 editors.len(),
21529 2,
21530 "After falling back to references search, we open a new editor with the results"
21531 );
21532 let references_fallback_text = editors
21533 .into_iter()
21534 .find(|new_editor| *new_editor != test_editor_cx.entity())
21535 .expect("Should have one non-test editor now")
21536 .read(test_editor_cx)
21537 .text(test_editor_cx);
21538 assert_eq!(
21539 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21540 "Should use the range from the references response and not the GoToDefinition one"
21541 );
21542 });
21543}
21544
21545#[gpui::test]
21546async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21547 init_test(cx, |_| {});
21548 cx.update(|cx| {
21549 let mut editor_settings = EditorSettings::get_global(cx).clone();
21550 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21551 EditorSettings::override_global(editor_settings, cx);
21552 });
21553 let mut cx = EditorLspTestContext::new_rust(
21554 lsp::ServerCapabilities {
21555 definition_provider: Some(lsp::OneOf::Left(true)),
21556 references_provider: Some(lsp::OneOf::Left(true)),
21557 ..lsp::ServerCapabilities::default()
21558 },
21559 cx,
21560 )
21561 .await;
21562 let original_state = r#"fn one() {
21563 let mut a = ˇtwo();
21564 }
21565
21566 fn two() {}"#
21567 .unindent();
21568 cx.set_state(&original_state);
21569
21570 let mut go_to_definition = cx
21571 .lsp
21572 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21573 move |_, _| async move { Ok(None) },
21574 );
21575 let _references = cx
21576 .lsp
21577 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21578 panic!("Should not call for references with no go to definition fallback")
21579 });
21580
21581 let navigated = cx
21582 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21583 .await
21584 .expect("Failed to navigate to lookup references");
21585 go_to_definition
21586 .next()
21587 .await
21588 .expect("Should have called the go_to_definition handler");
21589
21590 assert_eq!(
21591 navigated,
21592 Navigated::No,
21593 "Should have navigated to references as a fallback after empty GoToDefinition response"
21594 );
21595 cx.assert_editor_state(&original_state);
21596 let editors = cx.update_workspace(|workspace, _, cx| {
21597 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21598 });
21599 cx.update_editor(|_, _, _| {
21600 assert_eq!(
21601 editors.len(),
21602 1,
21603 "After unsuccessful fallback, no other editor should have been opened"
21604 );
21605 });
21606}
21607
21608#[gpui::test]
21609async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21610 init_test(cx, |_| {});
21611 let mut cx = EditorLspTestContext::new_rust(
21612 lsp::ServerCapabilities {
21613 references_provider: Some(lsp::OneOf::Left(true)),
21614 ..lsp::ServerCapabilities::default()
21615 },
21616 cx,
21617 )
21618 .await;
21619
21620 cx.set_state(
21621 &r#"
21622 fn one() {
21623 let mut a = two();
21624 }
21625
21626 fn ˇtwo() {}"#
21627 .unindent(),
21628 );
21629 cx.lsp
21630 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21631 Ok(Some(vec![
21632 lsp::Location {
21633 uri: params.text_document_position.text_document.uri.clone(),
21634 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21635 },
21636 lsp::Location {
21637 uri: params.text_document_position.text_document.uri,
21638 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21639 },
21640 ]))
21641 });
21642 let navigated = cx
21643 .update_editor(|editor, window, cx| {
21644 editor.find_all_references(&FindAllReferences, window, cx)
21645 })
21646 .unwrap()
21647 .await
21648 .expect("Failed to navigate to references");
21649 assert_eq!(
21650 navigated,
21651 Navigated::Yes,
21652 "Should have navigated to references from the FindAllReferences response"
21653 );
21654 cx.assert_editor_state(
21655 &r#"fn one() {
21656 let mut a = two();
21657 }
21658
21659 fn ˇtwo() {}"#
21660 .unindent(),
21661 );
21662
21663 let editors = cx.update_workspace(|workspace, _, cx| {
21664 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21665 });
21666 cx.update_editor(|_, _, _| {
21667 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21668 });
21669
21670 cx.set_state(
21671 &r#"fn one() {
21672 let mut a = ˇtwo();
21673 }
21674
21675 fn two() {}"#
21676 .unindent(),
21677 );
21678 let navigated = cx
21679 .update_editor(|editor, window, cx| {
21680 editor.find_all_references(&FindAllReferences, window, cx)
21681 })
21682 .unwrap()
21683 .await
21684 .expect("Failed to navigate to references");
21685 assert_eq!(
21686 navigated,
21687 Navigated::Yes,
21688 "Should have navigated to references from the FindAllReferences response"
21689 );
21690 cx.assert_editor_state(
21691 &r#"fn one() {
21692 let mut a = ˇtwo();
21693 }
21694
21695 fn two() {}"#
21696 .unindent(),
21697 );
21698 let editors = cx.update_workspace(|workspace, _, cx| {
21699 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21700 });
21701 cx.update_editor(|_, _, _| {
21702 assert_eq!(
21703 editors.len(),
21704 2,
21705 "should have re-used the previous multibuffer"
21706 );
21707 });
21708
21709 cx.set_state(
21710 &r#"fn one() {
21711 let mut a = ˇtwo();
21712 }
21713 fn three() {}
21714 fn two() {}"#
21715 .unindent(),
21716 );
21717 cx.lsp
21718 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21719 Ok(Some(vec![
21720 lsp::Location {
21721 uri: params.text_document_position.text_document.uri.clone(),
21722 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21723 },
21724 lsp::Location {
21725 uri: params.text_document_position.text_document.uri,
21726 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21727 },
21728 ]))
21729 });
21730 let navigated = cx
21731 .update_editor(|editor, window, cx| {
21732 editor.find_all_references(&FindAllReferences, window, cx)
21733 })
21734 .unwrap()
21735 .await
21736 .expect("Failed to navigate to references");
21737 assert_eq!(
21738 navigated,
21739 Navigated::Yes,
21740 "Should have navigated to references from the FindAllReferences response"
21741 );
21742 cx.assert_editor_state(
21743 &r#"fn one() {
21744 let mut a = ˇtwo();
21745 }
21746 fn three() {}
21747 fn two() {}"#
21748 .unindent(),
21749 );
21750 let editors = cx.update_workspace(|workspace, _, cx| {
21751 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21752 });
21753 cx.update_editor(|_, _, _| {
21754 assert_eq!(
21755 editors.len(),
21756 3,
21757 "should have used a new multibuffer as offsets changed"
21758 );
21759 });
21760}
21761#[gpui::test]
21762async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21763 init_test(cx, |_| {});
21764
21765 let language = Arc::new(Language::new(
21766 LanguageConfig::default(),
21767 Some(tree_sitter_rust::LANGUAGE.into()),
21768 ));
21769
21770 let text = r#"
21771 #[cfg(test)]
21772 mod tests() {
21773 #[test]
21774 fn runnable_1() {
21775 let a = 1;
21776 }
21777
21778 #[test]
21779 fn runnable_2() {
21780 let a = 1;
21781 let b = 2;
21782 }
21783 }
21784 "#
21785 .unindent();
21786
21787 let fs = FakeFs::new(cx.executor());
21788 fs.insert_file("/file.rs", Default::default()).await;
21789
21790 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21791 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21792 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21793 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21794 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21795
21796 let editor = cx.new_window_entity(|window, cx| {
21797 Editor::new(
21798 EditorMode::full(),
21799 multi_buffer,
21800 Some(project.clone()),
21801 window,
21802 cx,
21803 )
21804 });
21805
21806 editor.update_in(cx, |editor, window, cx| {
21807 let snapshot = editor.buffer().read(cx).snapshot(cx);
21808 editor.tasks.insert(
21809 (buffer.read(cx).remote_id(), 3),
21810 RunnableTasks {
21811 templates: vec![],
21812 offset: snapshot.anchor_before(43),
21813 column: 0,
21814 extra_variables: HashMap::default(),
21815 context_range: BufferOffset(43)..BufferOffset(85),
21816 },
21817 );
21818 editor.tasks.insert(
21819 (buffer.read(cx).remote_id(), 8),
21820 RunnableTasks {
21821 templates: vec![],
21822 offset: snapshot.anchor_before(86),
21823 column: 0,
21824 extra_variables: HashMap::default(),
21825 context_range: BufferOffset(86)..BufferOffset(191),
21826 },
21827 );
21828
21829 // Test finding task when cursor is inside function body
21830 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21831 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21832 });
21833 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21834 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21835
21836 // Test finding task when cursor is on function name
21837 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21838 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21839 });
21840 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21841 assert_eq!(row, 8, "Should find task when cursor is on function name");
21842 });
21843}
21844
21845#[gpui::test]
21846async fn test_folding_buffers(cx: &mut TestAppContext) {
21847 init_test(cx, |_| {});
21848
21849 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21850 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21851 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21852
21853 let fs = FakeFs::new(cx.executor());
21854 fs.insert_tree(
21855 path!("/a"),
21856 json!({
21857 "first.rs": sample_text_1,
21858 "second.rs": sample_text_2,
21859 "third.rs": sample_text_3,
21860 }),
21861 )
21862 .await;
21863 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21864 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21865 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21866 let worktree = project.update(cx, |project, cx| {
21867 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21868 assert_eq!(worktrees.len(), 1);
21869 worktrees.pop().unwrap()
21870 });
21871 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21872
21873 let buffer_1 = project
21874 .update(cx, |project, cx| {
21875 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
21876 })
21877 .await
21878 .unwrap();
21879 let buffer_2 = project
21880 .update(cx, |project, cx| {
21881 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
21882 })
21883 .await
21884 .unwrap();
21885 let buffer_3 = project
21886 .update(cx, |project, cx| {
21887 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
21888 })
21889 .await
21890 .unwrap();
21891
21892 let multi_buffer = cx.new(|cx| {
21893 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21894 multi_buffer.push_excerpts(
21895 buffer_1.clone(),
21896 [
21897 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21898 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21899 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21900 ],
21901 cx,
21902 );
21903 multi_buffer.push_excerpts(
21904 buffer_2.clone(),
21905 [
21906 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21907 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21908 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21909 ],
21910 cx,
21911 );
21912 multi_buffer.push_excerpts(
21913 buffer_3.clone(),
21914 [
21915 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21916 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21917 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21918 ],
21919 cx,
21920 );
21921 multi_buffer
21922 });
21923 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21924 Editor::new(
21925 EditorMode::full(),
21926 multi_buffer.clone(),
21927 Some(project.clone()),
21928 window,
21929 cx,
21930 )
21931 });
21932
21933 assert_eq!(
21934 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21935 "\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",
21936 );
21937
21938 multi_buffer_editor.update(cx, |editor, cx| {
21939 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21940 });
21941 assert_eq!(
21942 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21943 "\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",
21944 "After folding the first buffer, its text should not be displayed"
21945 );
21946
21947 multi_buffer_editor.update(cx, |editor, cx| {
21948 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21949 });
21950 assert_eq!(
21951 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21952 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21953 "After folding the second buffer, its text should not be displayed"
21954 );
21955
21956 multi_buffer_editor.update(cx, |editor, cx| {
21957 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21958 });
21959 assert_eq!(
21960 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21961 "\n\n\n\n\n",
21962 "After folding the third buffer, its text should not be displayed"
21963 );
21964
21965 // Emulate selection inside the fold logic, that should work
21966 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21967 editor
21968 .snapshot(window, cx)
21969 .next_line_boundary(Point::new(0, 4));
21970 });
21971
21972 multi_buffer_editor.update(cx, |editor, cx| {
21973 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21974 });
21975 assert_eq!(
21976 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21977 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21978 "After unfolding the second buffer, its text should be displayed"
21979 );
21980
21981 // Typing inside of buffer 1 causes that buffer to be unfolded.
21982 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21983 assert_eq!(
21984 multi_buffer
21985 .read(cx)
21986 .snapshot(cx)
21987 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21988 .collect::<String>(),
21989 "bbbb"
21990 );
21991 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21992 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
21993 });
21994 editor.handle_input("B", window, cx);
21995 });
21996
21997 assert_eq!(
21998 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21999 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22000 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22001 );
22002
22003 multi_buffer_editor.update(cx, |editor, cx| {
22004 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22005 });
22006 assert_eq!(
22007 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22008 "\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",
22009 "After unfolding the all buffers, all original text should be displayed"
22010 );
22011}
22012
22013#[gpui::test]
22014async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22015 init_test(cx, |_| {});
22016
22017 let sample_text_1 = "1111\n2222\n3333".to_string();
22018 let sample_text_2 = "4444\n5555\n6666".to_string();
22019 let sample_text_3 = "7777\n8888\n9999".to_string();
22020
22021 let fs = FakeFs::new(cx.executor());
22022 fs.insert_tree(
22023 path!("/a"),
22024 json!({
22025 "first.rs": sample_text_1,
22026 "second.rs": sample_text_2,
22027 "third.rs": sample_text_3,
22028 }),
22029 )
22030 .await;
22031 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22032 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22033 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22034 let worktree = project.update(cx, |project, cx| {
22035 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22036 assert_eq!(worktrees.len(), 1);
22037 worktrees.pop().unwrap()
22038 });
22039 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22040
22041 let buffer_1 = project
22042 .update(cx, |project, cx| {
22043 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22044 })
22045 .await
22046 .unwrap();
22047 let buffer_2 = project
22048 .update(cx, |project, cx| {
22049 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22050 })
22051 .await
22052 .unwrap();
22053 let buffer_3 = project
22054 .update(cx, |project, cx| {
22055 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22056 })
22057 .await
22058 .unwrap();
22059
22060 let multi_buffer = cx.new(|cx| {
22061 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22062 multi_buffer.push_excerpts(
22063 buffer_1.clone(),
22064 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22065 cx,
22066 );
22067 multi_buffer.push_excerpts(
22068 buffer_2.clone(),
22069 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22070 cx,
22071 );
22072 multi_buffer.push_excerpts(
22073 buffer_3.clone(),
22074 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22075 cx,
22076 );
22077 multi_buffer
22078 });
22079
22080 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22081 Editor::new(
22082 EditorMode::full(),
22083 multi_buffer,
22084 Some(project.clone()),
22085 window,
22086 cx,
22087 )
22088 });
22089
22090 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22091 assert_eq!(
22092 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22093 full_text,
22094 );
22095
22096 multi_buffer_editor.update(cx, |editor, cx| {
22097 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22098 });
22099 assert_eq!(
22100 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22101 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22102 "After folding the first buffer, its text should not be displayed"
22103 );
22104
22105 multi_buffer_editor.update(cx, |editor, cx| {
22106 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22107 });
22108
22109 assert_eq!(
22110 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22111 "\n\n\n\n\n\n7777\n8888\n9999",
22112 "After folding the second buffer, its text should not be displayed"
22113 );
22114
22115 multi_buffer_editor.update(cx, |editor, cx| {
22116 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22117 });
22118 assert_eq!(
22119 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22120 "\n\n\n\n\n",
22121 "After folding the third buffer, its text should not be displayed"
22122 );
22123
22124 multi_buffer_editor.update(cx, |editor, cx| {
22125 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22126 });
22127 assert_eq!(
22128 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22129 "\n\n\n\n4444\n5555\n6666\n\n",
22130 "After unfolding the second buffer, its text should be displayed"
22131 );
22132
22133 multi_buffer_editor.update(cx, |editor, cx| {
22134 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22135 });
22136 assert_eq!(
22137 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22138 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22139 "After unfolding the first buffer, its text should be displayed"
22140 );
22141
22142 multi_buffer_editor.update(cx, |editor, cx| {
22143 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22144 });
22145 assert_eq!(
22146 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22147 full_text,
22148 "After unfolding all buffers, all original text should be displayed"
22149 );
22150}
22151
22152#[gpui::test]
22153async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22154 init_test(cx, |_| {});
22155
22156 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22157
22158 let fs = FakeFs::new(cx.executor());
22159 fs.insert_tree(
22160 path!("/a"),
22161 json!({
22162 "main.rs": sample_text,
22163 }),
22164 )
22165 .await;
22166 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22167 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22168 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22169 let worktree = project.update(cx, |project, cx| {
22170 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22171 assert_eq!(worktrees.len(), 1);
22172 worktrees.pop().unwrap()
22173 });
22174 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22175
22176 let buffer_1 = project
22177 .update(cx, |project, cx| {
22178 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22179 })
22180 .await
22181 .unwrap();
22182
22183 let multi_buffer = cx.new(|cx| {
22184 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22185 multi_buffer.push_excerpts(
22186 buffer_1.clone(),
22187 [ExcerptRange::new(
22188 Point::new(0, 0)
22189 ..Point::new(
22190 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22191 0,
22192 ),
22193 )],
22194 cx,
22195 );
22196 multi_buffer
22197 });
22198 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22199 Editor::new(
22200 EditorMode::full(),
22201 multi_buffer,
22202 Some(project.clone()),
22203 window,
22204 cx,
22205 )
22206 });
22207
22208 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22209 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22210 enum TestHighlight {}
22211 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22212 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22213 editor.highlight_text::<TestHighlight>(
22214 vec![highlight_range.clone()],
22215 HighlightStyle::color(Hsla::green()),
22216 cx,
22217 );
22218 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22219 s.select_ranges(Some(highlight_range))
22220 });
22221 });
22222
22223 let full_text = format!("\n\n{sample_text}");
22224 assert_eq!(
22225 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22226 full_text,
22227 );
22228}
22229
22230#[gpui::test]
22231async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22232 init_test(cx, |_| {});
22233 cx.update(|cx| {
22234 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22235 "keymaps/default-linux.json",
22236 cx,
22237 )
22238 .unwrap();
22239 cx.bind_keys(default_key_bindings);
22240 });
22241
22242 let (editor, cx) = cx.add_window_view(|window, cx| {
22243 let multi_buffer = MultiBuffer::build_multi(
22244 [
22245 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22246 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22247 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22248 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22249 ],
22250 cx,
22251 );
22252 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22253
22254 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22255 // fold all but the second buffer, so that we test navigating between two
22256 // adjacent folded buffers, as well as folded buffers at the start and
22257 // end the multibuffer
22258 editor.fold_buffer(buffer_ids[0], cx);
22259 editor.fold_buffer(buffer_ids[2], cx);
22260 editor.fold_buffer(buffer_ids[3], cx);
22261
22262 editor
22263 });
22264 cx.simulate_resize(size(px(1000.), px(1000.)));
22265
22266 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22267 cx.assert_excerpts_with_selections(indoc! {"
22268 [EXCERPT]
22269 ˇ[FOLDED]
22270 [EXCERPT]
22271 a1
22272 b1
22273 [EXCERPT]
22274 [FOLDED]
22275 [EXCERPT]
22276 [FOLDED]
22277 "
22278 });
22279 cx.simulate_keystroke("down");
22280 cx.assert_excerpts_with_selections(indoc! {"
22281 [EXCERPT]
22282 [FOLDED]
22283 [EXCERPT]
22284 ˇa1
22285 b1
22286 [EXCERPT]
22287 [FOLDED]
22288 [EXCERPT]
22289 [FOLDED]
22290 "
22291 });
22292 cx.simulate_keystroke("down");
22293 cx.assert_excerpts_with_selections(indoc! {"
22294 [EXCERPT]
22295 [FOLDED]
22296 [EXCERPT]
22297 a1
22298 ˇb1
22299 [EXCERPT]
22300 [FOLDED]
22301 [EXCERPT]
22302 [FOLDED]
22303 "
22304 });
22305 cx.simulate_keystroke("down");
22306 cx.assert_excerpts_with_selections(indoc! {"
22307 [EXCERPT]
22308 [FOLDED]
22309 [EXCERPT]
22310 a1
22311 b1
22312 ˇ[EXCERPT]
22313 [FOLDED]
22314 [EXCERPT]
22315 [FOLDED]
22316 "
22317 });
22318 cx.simulate_keystroke("down");
22319 cx.assert_excerpts_with_selections(indoc! {"
22320 [EXCERPT]
22321 [FOLDED]
22322 [EXCERPT]
22323 a1
22324 b1
22325 [EXCERPT]
22326 ˇ[FOLDED]
22327 [EXCERPT]
22328 [FOLDED]
22329 "
22330 });
22331 for _ in 0..5 {
22332 cx.simulate_keystroke("down");
22333 cx.assert_excerpts_with_selections(indoc! {"
22334 [EXCERPT]
22335 [FOLDED]
22336 [EXCERPT]
22337 a1
22338 b1
22339 [EXCERPT]
22340 [FOLDED]
22341 [EXCERPT]
22342 ˇ[FOLDED]
22343 "
22344 });
22345 }
22346
22347 cx.simulate_keystroke("up");
22348 cx.assert_excerpts_with_selections(indoc! {"
22349 [EXCERPT]
22350 [FOLDED]
22351 [EXCERPT]
22352 a1
22353 b1
22354 [EXCERPT]
22355 ˇ[FOLDED]
22356 [EXCERPT]
22357 [FOLDED]
22358 "
22359 });
22360 cx.simulate_keystroke("up");
22361 cx.assert_excerpts_with_selections(indoc! {"
22362 [EXCERPT]
22363 [FOLDED]
22364 [EXCERPT]
22365 a1
22366 b1
22367 ˇ[EXCERPT]
22368 [FOLDED]
22369 [EXCERPT]
22370 [FOLDED]
22371 "
22372 });
22373 cx.simulate_keystroke("up");
22374 cx.assert_excerpts_with_selections(indoc! {"
22375 [EXCERPT]
22376 [FOLDED]
22377 [EXCERPT]
22378 a1
22379 ˇb1
22380 [EXCERPT]
22381 [FOLDED]
22382 [EXCERPT]
22383 [FOLDED]
22384 "
22385 });
22386 cx.simulate_keystroke("up");
22387 cx.assert_excerpts_with_selections(indoc! {"
22388 [EXCERPT]
22389 [FOLDED]
22390 [EXCERPT]
22391 ˇa1
22392 b1
22393 [EXCERPT]
22394 [FOLDED]
22395 [EXCERPT]
22396 [FOLDED]
22397 "
22398 });
22399 for _ in 0..5 {
22400 cx.simulate_keystroke("up");
22401 cx.assert_excerpts_with_selections(indoc! {"
22402 [EXCERPT]
22403 ˇ[FOLDED]
22404 [EXCERPT]
22405 a1
22406 b1
22407 [EXCERPT]
22408 [FOLDED]
22409 [EXCERPT]
22410 [FOLDED]
22411 "
22412 });
22413 }
22414}
22415
22416#[gpui::test]
22417async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22418 init_test(cx, |_| {});
22419
22420 // Simple insertion
22421 assert_highlighted_edits(
22422 "Hello, world!",
22423 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22424 true,
22425 cx,
22426 |highlighted_edits, cx| {
22427 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22428 assert_eq!(highlighted_edits.highlights.len(), 1);
22429 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22430 assert_eq!(
22431 highlighted_edits.highlights[0].1.background_color,
22432 Some(cx.theme().status().created_background)
22433 );
22434 },
22435 )
22436 .await;
22437
22438 // Replacement
22439 assert_highlighted_edits(
22440 "This is a test.",
22441 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22442 false,
22443 cx,
22444 |highlighted_edits, cx| {
22445 assert_eq!(highlighted_edits.text, "That is a test.");
22446 assert_eq!(highlighted_edits.highlights.len(), 1);
22447 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22448 assert_eq!(
22449 highlighted_edits.highlights[0].1.background_color,
22450 Some(cx.theme().status().created_background)
22451 );
22452 },
22453 )
22454 .await;
22455
22456 // Multiple edits
22457 assert_highlighted_edits(
22458 "Hello, world!",
22459 vec![
22460 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22461 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22462 ],
22463 false,
22464 cx,
22465 |highlighted_edits, cx| {
22466 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22467 assert_eq!(highlighted_edits.highlights.len(), 2);
22468 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22469 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22470 assert_eq!(
22471 highlighted_edits.highlights[0].1.background_color,
22472 Some(cx.theme().status().created_background)
22473 );
22474 assert_eq!(
22475 highlighted_edits.highlights[1].1.background_color,
22476 Some(cx.theme().status().created_background)
22477 );
22478 },
22479 )
22480 .await;
22481
22482 // Multiple lines with edits
22483 assert_highlighted_edits(
22484 "First line\nSecond line\nThird line\nFourth line",
22485 vec![
22486 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22487 (
22488 Point::new(2, 0)..Point::new(2, 10),
22489 "New third line".to_string(),
22490 ),
22491 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22492 ],
22493 false,
22494 cx,
22495 |highlighted_edits, cx| {
22496 assert_eq!(
22497 highlighted_edits.text,
22498 "Second modified\nNew third line\nFourth updated line"
22499 );
22500 assert_eq!(highlighted_edits.highlights.len(), 3);
22501 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22502 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22503 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22504 for highlight in &highlighted_edits.highlights {
22505 assert_eq!(
22506 highlight.1.background_color,
22507 Some(cx.theme().status().created_background)
22508 );
22509 }
22510 },
22511 )
22512 .await;
22513}
22514
22515#[gpui::test]
22516async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22517 init_test(cx, |_| {});
22518
22519 // Deletion
22520 assert_highlighted_edits(
22521 "Hello, world!",
22522 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22523 true,
22524 cx,
22525 |highlighted_edits, cx| {
22526 assert_eq!(highlighted_edits.text, "Hello, world!");
22527 assert_eq!(highlighted_edits.highlights.len(), 1);
22528 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22529 assert_eq!(
22530 highlighted_edits.highlights[0].1.background_color,
22531 Some(cx.theme().status().deleted_background)
22532 );
22533 },
22534 )
22535 .await;
22536
22537 // Insertion
22538 assert_highlighted_edits(
22539 "Hello, world!",
22540 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22541 true,
22542 cx,
22543 |highlighted_edits, cx| {
22544 assert_eq!(highlighted_edits.highlights.len(), 1);
22545 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22546 assert_eq!(
22547 highlighted_edits.highlights[0].1.background_color,
22548 Some(cx.theme().status().created_background)
22549 );
22550 },
22551 )
22552 .await;
22553}
22554
22555async fn assert_highlighted_edits(
22556 text: &str,
22557 edits: Vec<(Range<Point>, String)>,
22558 include_deletions: bool,
22559 cx: &mut TestAppContext,
22560 assertion_fn: impl Fn(HighlightedText, &App),
22561) {
22562 let window = cx.add_window(|window, cx| {
22563 let buffer = MultiBuffer::build_simple(text, cx);
22564 Editor::new(EditorMode::full(), buffer, None, window, cx)
22565 });
22566 let cx = &mut VisualTestContext::from_window(*window, cx);
22567
22568 let (buffer, snapshot) = window
22569 .update(cx, |editor, _window, cx| {
22570 (
22571 editor.buffer().clone(),
22572 editor.buffer().read(cx).snapshot(cx),
22573 )
22574 })
22575 .unwrap();
22576
22577 let edits = edits
22578 .into_iter()
22579 .map(|(range, edit)| {
22580 (
22581 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22582 edit,
22583 )
22584 })
22585 .collect::<Vec<_>>();
22586
22587 let text_anchor_edits = edits
22588 .clone()
22589 .into_iter()
22590 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22591 .collect::<Vec<_>>();
22592
22593 let edit_preview = window
22594 .update(cx, |_, _window, cx| {
22595 buffer
22596 .read(cx)
22597 .as_singleton()
22598 .unwrap()
22599 .read(cx)
22600 .preview_edits(text_anchor_edits.into(), cx)
22601 })
22602 .unwrap()
22603 .await;
22604
22605 cx.update(|_window, cx| {
22606 let highlighted_edits = edit_prediction_edit_text(
22607 snapshot.as_singleton().unwrap().2,
22608 &edits,
22609 &edit_preview,
22610 include_deletions,
22611 cx,
22612 );
22613 assertion_fn(highlighted_edits, cx)
22614 });
22615}
22616
22617#[track_caller]
22618fn assert_breakpoint(
22619 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22620 path: &Arc<Path>,
22621 expected: Vec<(u32, Breakpoint)>,
22622) {
22623 if expected.is_empty() {
22624 assert!(!breakpoints.contains_key(path), "{}", path.display());
22625 } else {
22626 let mut breakpoint = breakpoints
22627 .get(path)
22628 .unwrap()
22629 .iter()
22630 .map(|breakpoint| {
22631 (
22632 breakpoint.row,
22633 Breakpoint {
22634 message: breakpoint.message.clone(),
22635 state: breakpoint.state,
22636 condition: breakpoint.condition.clone(),
22637 hit_condition: breakpoint.hit_condition.clone(),
22638 },
22639 )
22640 })
22641 .collect::<Vec<_>>();
22642
22643 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22644
22645 assert_eq!(expected, breakpoint);
22646 }
22647}
22648
22649fn add_log_breakpoint_at_cursor(
22650 editor: &mut Editor,
22651 log_message: &str,
22652 window: &mut Window,
22653 cx: &mut Context<Editor>,
22654) {
22655 let (anchor, bp) = editor
22656 .breakpoints_at_cursors(window, cx)
22657 .first()
22658 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22659 .unwrap_or_else(|| {
22660 let cursor_position: Point = editor.selections.newest(cx).head();
22661
22662 let breakpoint_position = editor
22663 .snapshot(window, cx)
22664 .display_snapshot
22665 .buffer_snapshot()
22666 .anchor_before(Point::new(cursor_position.row, 0));
22667
22668 (breakpoint_position, Breakpoint::new_log(log_message))
22669 });
22670
22671 editor.edit_breakpoint_at_anchor(
22672 anchor,
22673 bp,
22674 BreakpointEditAction::EditLogMessage(log_message.into()),
22675 cx,
22676 );
22677}
22678
22679#[gpui::test]
22680async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22681 init_test(cx, |_| {});
22682
22683 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22684 let fs = FakeFs::new(cx.executor());
22685 fs.insert_tree(
22686 path!("/a"),
22687 json!({
22688 "main.rs": sample_text,
22689 }),
22690 )
22691 .await;
22692 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22693 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22694 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22695
22696 let fs = FakeFs::new(cx.executor());
22697 fs.insert_tree(
22698 path!("/a"),
22699 json!({
22700 "main.rs": sample_text,
22701 }),
22702 )
22703 .await;
22704 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22705 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22706 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22707 let worktree_id = workspace
22708 .update(cx, |workspace, _window, cx| {
22709 workspace.project().update(cx, |project, cx| {
22710 project.worktrees(cx).next().unwrap().read(cx).id()
22711 })
22712 })
22713 .unwrap();
22714
22715 let buffer = project
22716 .update(cx, |project, cx| {
22717 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22718 })
22719 .await
22720 .unwrap();
22721
22722 let (editor, cx) = cx.add_window_view(|window, cx| {
22723 Editor::new(
22724 EditorMode::full(),
22725 MultiBuffer::build_from_buffer(buffer, cx),
22726 Some(project.clone()),
22727 window,
22728 cx,
22729 )
22730 });
22731
22732 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22733 let abs_path = project.read_with(cx, |project, cx| {
22734 project
22735 .absolute_path(&project_path, cx)
22736 .map(Arc::from)
22737 .unwrap()
22738 });
22739
22740 // assert we can add breakpoint on the first line
22741 editor.update_in(cx, |editor, window, cx| {
22742 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22743 editor.move_to_end(&MoveToEnd, window, cx);
22744 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22745 });
22746
22747 let breakpoints = editor.update(cx, |editor, cx| {
22748 editor
22749 .breakpoint_store()
22750 .as_ref()
22751 .unwrap()
22752 .read(cx)
22753 .all_source_breakpoints(cx)
22754 });
22755
22756 assert_eq!(1, breakpoints.len());
22757 assert_breakpoint(
22758 &breakpoints,
22759 &abs_path,
22760 vec![
22761 (0, Breakpoint::new_standard()),
22762 (3, Breakpoint::new_standard()),
22763 ],
22764 );
22765
22766 editor.update_in(cx, |editor, window, cx| {
22767 editor.move_to_beginning(&MoveToBeginning, window, cx);
22768 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22769 });
22770
22771 let breakpoints = editor.update(cx, |editor, cx| {
22772 editor
22773 .breakpoint_store()
22774 .as_ref()
22775 .unwrap()
22776 .read(cx)
22777 .all_source_breakpoints(cx)
22778 });
22779
22780 assert_eq!(1, breakpoints.len());
22781 assert_breakpoint(
22782 &breakpoints,
22783 &abs_path,
22784 vec![(3, Breakpoint::new_standard())],
22785 );
22786
22787 editor.update_in(cx, |editor, window, cx| {
22788 editor.move_to_end(&MoveToEnd, window, cx);
22789 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22790 });
22791
22792 let breakpoints = editor.update(cx, |editor, cx| {
22793 editor
22794 .breakpoint_store()
22795 .as_ref()
22796 .unwrap()
22797 .read(cx)
22798 .all_source_breakpoints(cx)
22799 });
22800
22801 assert_eq!(0, breakpoints.len());
22802 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22803}
22804
22805#[gpui::test]
22806async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22807 init_test(cx, |_| {});
22808
22809 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22810
22811 let fs = FakeFs::new(cx.executor());
22812 fs.insert_tree(
22813 path!("/a"),
22814 json!({
22815 "main.rs": sample_text,
22816 }),
22817 )
22818 .await;
22819 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22820 let (workspace, cx) =
22821 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22822
22823 let worktree_id = workspace.update(cx, |workspace, cx| {
22824 workspace.project().update(cx, |project, cx| {
22825 project.worktrees(cx).next().unwrap().read(cx).id()
22826 })
22827 });
22828
22829 let buffer = project
22830 .update(cx, |project, cx| {
22831 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22832 })
22833 .await
22834 .unwrap();
22835
22836 let (editor, cx) = cx.add_window_view(|window, cx| {
22837 Editor::new(
22838 EditorMode::full(),
22839 MultiBuffer::build_from_buffer(buffer, cx),
22840 Some(project.clone()),
22841 window,
22842 cx,
22843 )
22844 });
22845
22846 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22847 let abs_path = project.read_with(cx, |project, cx| {
22848 project
22849 .absolute_path(&project_path, cx)
22850 .map(Arc::from)
22851 .unwrap()
22852 });
22853
22854 editor.update_in(cx, |editor, window, cx| {
22855 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22856 });
22857
22858 let breakpoints = editor.update(cx, |editor, cx| {
22859 editor
22860 .breakpoint_store()
22861 .as_ref()
22862 .unwrap()
22863 .read(cx)
22864 .all_source_breakpoints(cx)
22865 });
22866
22867 assert_breakpoint(
22868 &breakpoints,
22869 &abs_path,
22870 vec![(0, Breakpoint::new_log("hello world"))],
22871 );
22872
22873 // Removing a log message from a log breakpoint should remove it
22874 editor.update_in(cx, |editor, window, cx| {
22875 add_log_breakpoint_at_cursor(editor, "", window, cx);
22876 });
22877
22878 let breakpoints = editor.update(cx, |editor, cx| {
22879 editor
22880 .breakpoint_store()
22881 .as_ref()
22882 .unwrap()
22883 .read(cx)
22884 .all_source_breakpoints(cx)
22885 });
22886
22887 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22888
22889 editor.update_in(cx, |editor, window, cx| {
22890 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22891 editor.move_to_end(&MoveToEnd, window, cx);
22892 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22893 // Not adding a log message to a standard breakpoint shouldn't remove it
22894 add_log_breakpoint_at_cursor(editor, "", window, cx);
22895 });
22896
22897 let breakpoints = editor.update(cx, |editor, cx| {
22898 editor
22899 .breakpoint_store()
22900 .as_ref()
22901 .unwrap()
22902 .read(cx)
22903 .all_source_breakpoints(cx)
22904 });
22905
22906 assert_breakpoint(
22907 &breakpoints,
22908 &abs_path,
22909 vec![
22910 (0, Breakpoint::new_standard()),
22911 (3, Breakpoint::new_standard()),
22912 ],
22913 );
22914
22915 editor.update_in(cx, |editor, window, cx| {
22916 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22917 });
22918
22919 let breakpoints = editor.update(cx, |editor, cx| {
22920 editor
22921 .breakpoint_store()
22922 .as_ref()
22923 .unwrap()
22924 .read(cx)
22925 .all_source_breakpoints(cx)
22926 });
22927
22928 assert_breakpoint(
22929 &breakpoints,
22930 &abs_path,
22931 vec![
22932 (0, Breakpoint::new_standard()),
22933 (3, Breakpoint::new_log("hello world")),
22934 ],
22935 );
22936
22937 editor.update_in(cx, |editor, window, cx| {
22938 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22939 });
22940
22941 let breakpoints = editor.update(cx, |editor, cx| {
22942 editor
22943 .breakpoint_store()
22944 .as_ref()
22945 .unwrap()
22946 .read(cx)
22947 .all_source_breakpoints(cx)
22948 });
22949
22950 assert_breakpoint(
22951 &breakpoints,
22952 &abs_path,
22953 vec![
22954 (0, Breakpoint::new_standard()),
22955 (3, Breakpoint::new_log("hello Earth!!")),
22956 ],
22957 );
22958}
22959
22960/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22961/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22962/// or when breakpoints were placed out of order. This tests for a regression too
22963#[gpui::test]
22964async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22965 init_test(cx, |_| {});
22966
22967 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22968 let fs = FakeFs::new(cx.executor());
22969 fs.insert_tree(
22970 path!("/a"),
22971 json!({
22972 "main.rs": sample_text,
22973 }),
22974 )
22975 .await;
22976 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22977 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22978 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22979
22980 let fs = FakeFs::new(cx.executor());
22981 fs.insert_tree(
22982 path!("/a"),
22983 json!({
22984 "main.rs": sample_text,
22985 }),
22986 )
22987 .await;
22988 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22989 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22990 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22991 let worktree_id = workspace
22992 .update(cx, |workspace, _window, cx| {
22993 workspace.project().update(cx, |project, cx| {
22994 project.worktrees(cx).next().unwrap().read(cx).id()
22995 })
22996 })
22997 .unwrap();
22998
22999 let buffer = project
23000 .update(cx, |project, cx| {
23001 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23002 })
23003 .await
23004 .unwrap();
23005
23006 let (editor, cx) = cx.add_window_view(|window, cx| {
23007 Editor::new(
23008 EditorMode::full(),
23009 MultiBuffer::build_from_buffer(buffer, cx),
23010 Some(project.clone()),
23011 window,
23012 cx,
23013 )
23014 });
23015
23016 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23017 let abs_path = project.read_with(cx, |project, cx| {
23018 project
23019 .absolute_path(&project_path, cx)
23020 .map(Arc::from)
23021 .unwrap()
23022 });
23023
23024 // assert we can add breakpoint on the first line
23025 editor.update_in(cx, |editor, window, cx| {
23026 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23027 editor.move_to_end(&MoveToEnd, window, cx);
23028 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23029 editor.move_up(&MoveUp, window, cx);
23030 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23031 });
23032
23033 let breakpoints = editor.update(cx, |editor, cx| {
23034 editor
23035 .breakpoint_store()
23036 .as_ref()
23037 .unwrap()
23038 .read(cx)
23039 .all_source_breakpoints(cx)
23040 });
23041
23042 assert_eq!(1, breakpoints.len());
23043 assert_breakpoint(
23044 &breakpoints,
23045 &abs_path,
23046 vec![
23047 (0, Breakpoint::new_standard()),
23048 (2, Breakpoint::new_standard()),
23049 (3, Breakpoint::new_standard()),
23050 ],
23051 );
23052
23053 editor.update_in(cx, |editor, window, cx| {
23054 editor.move_to_beginning(&MoveToBeginning, window, cx);
23055 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23056 editor.move_to_end(&MoveToEnd, window, cx);
23057 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23058 // Disabling a breakpoint that doesn't exist should do nothing
23059 editor.move_up(&MoveUp, window, cx);
23060 editor.move_up(&MoveUp, window, cx);
23061 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23062 });
23063
23064 let breakpoints = editor.update(cx, |editor, cx| {
23065 editor
23066 .breakpoint_store()
23067 .as_ref()
23068 .unwrap()
23069 .read(cx)
23070 .all_source_breakpoints(cx)
23071 });
23072
23073 let disable_breakpoint = {
23074 let mut bp = Breakpoint::new_standard();
23075 bp.state = BreakpointState::Disabled;
23076 bp
23077 };
23078
23079 assert_eq!(1, breakpoints.len());
23080 assert_breakpoint(
23081 &breakpoints,
23082 &abs_path,
23083 vec![
23084 (0, disable_breakpoint.clone()),
23085 (2, Breakpoint::new_standard()),
23086 (3, disable_breakpoint.clone()),
23087 ],
23088 );
23089
23090 editor.update_in(cx, |editor, window, cx| {
23091 editor.move_to_beginning(&MoveToBeginning, window, cx);
23092 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23093 editor.move_to_end(&MoveToEnd, window, cx);
23094 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23095 editor.move_up(&MoveUp, window, cx);
23096 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23097 });
23098
23099 let breakpoints = editor.update(cx, |editor, cx| {
23100 editor
23101 .breakpoint_store()
23102 .as_ref()
23103 .unwrap()
23104 .read(cx)
23105 .all_source_breakpoints(cx)
23106 });
23107
23108 assert_eq!(1, breakpoints.len());
23109 assert_breakpoint(
23110 &breakpoints,
23111 &abs_path,
23112 vec![
23113 (0, Breakpoint::new_standard()),
23114 (2, disable_breakpoint),
23115 (3, Breakpoint::new_standard()),
23116 ],
23117 );
23118}
23119
23120#[gpui::test]
23121async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23122 init_test(cx, |_| {});
23123 let capabilities = lsp::ServerCapabilities {
23124 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23125 prepare_provider: Some(true),
23126 work_done_progress_options: Default::default(),
23127 })),
23128 ..Default::default()
23129 };
23130 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23131
23132 cx.set_state(indoc! {"
23133 struct Fˇoo {}
23134 "});
23135
23136 cx.update_editor(|editor, _, cx| {
23137 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23138 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23139 editor.highlight_background::<DocumentHighlightRead>(
23140 &[highlight_range],
23141 |theme| theme.colors().editor_document_highlight_read_background,
23142 cx,
23143 );
23144 });
23145
23146 let mut prepare_rename_handler = cx
23147 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23148 move |_, _, _| async move {
23149 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23150 start: lsp::Position {
23151 line: 0,
23152 character: 7,
23153 },
23154 end: lsp::Position {
23155 line: 0,
23156 character: 10,
23157 },
23158 })))
23159 },
23160 );
23161 let prepare_rename_task = cx
23162 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23163 .expect("Prepare rename was not started");
23164 prepare_rename_handler.next().await.unwrap();
23165 prepare_rename_task.await.expect("Prepare rename failed");
23166
23167 let mut rename_handler =
23168 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23169 let edit = lsp::TextEdit {
23170 range: lsp::Range {
23171 start: lsp::Position {
23172 line: 0,
23173 character: 7,
23174 },
23175 end: lsp::Position {
23176 line: 0,
23177 character: 10,
23178 },
23179 },
23180 new_text: "FooRenamed".to_string(),
23181 };
23182 Ok(Some(lsp::WorkspaceEdit::new(
23183 // Specify the same edit twice
23184 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23185 )))
23186 });
23187 let rename_task = cx
23188 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23189 .expect("Confirm rename was not started");
23190 rename_handler.next().await.unwrap();
23191 rename_task.await.expect("Confirm rename failed");
23192 cx.run_until_parked();
23193
23194 // Despite two edits, only one is actually applied as those are identical
23195 cx.assert_editor_state(indoc! {"
23196 struct FooRenamedˇ {}
23197 "});
23198}
23199
23200#[gpui::test]
23201async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23202 init_test(cx, |_| {});
23203 // These capabilities indicate that the server does not support prepare rename.
23204 let capabilities = lsp::ServerCapabilities {
23205 rename_provider: Some(lsp::OneOf::Left(true)),
23206 ..Default::default()
23207 };
23208 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23209
23210 cx.set_state(indoc! {"
23211 struct Fˇoo {}
23212 "});
23213
23214 cx.update_editor(|editor, _window, cx| {
23215 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23216 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23217 editor.highlight_background::<DocumentHighlightRead>(
23218 &[highlight_range],
23219 |theme| theme.colors().editor_document_highlight_read_background,
23220 cx,
23221 );
23222 });
23223
23224 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23225 .expect("Prepare rename was not started")
23226 .await
23227 .expect("Prepare rename failed");
23228
23229 let mut rename_handler =
23230 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23231 let edit = lsp::TextEdit {
23232 range: lsp::Range {
23233 start: lsp::Position {
23234 line: 0,
23235 character: 7,
23236 },
23237 end: lsp::Position {
23238 line: 0,
23239 character: 10,
23240 },
23241 },
23242 new_text: "FooRenamed".to_string(),
23243 };
23244 Ok(Some(lsp::WorkspaceEdit::new(
23245 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23246 )))
23247 });
23248 let rename_task = cx
23249 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23250 .expect("Confirm rename was not started");
23251 rename_handler.next().await.unwrap();
23252 rename_task.await.expect("Confirm rename failed");
23253 cx.run_until_parked();
23254
23255 // Correct range is renamed, as `surrounding_word` is used to find it.
23256 cx.assert_editor_state(indoc! {"
23257 struct FooRenamedˇ {}
23258 "});
23259}
23260
23261#[gpui::test]
23262async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23263 init_test(cx, |_| {});
23264 let mut cx = EditorTestContext::new(cx).await;
23265
23266 let language = Arc::new(
23267 Language::new(
23268 LanguageConfig::default(),
23269 Some(tree_sitter_html::LANGUAGE.into()),
23270 )
23271 .with_brackets_query(
23272 r#"
23273 ("<" @open "/>" @close)
23274 ("</" @open ">" @close)
23275 ("<" @open ">" @close)
23276 ("\"" @open "\"" @close)
23277 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23278 "#,
23279 )
23280 .unwrap(),
23281 );
23282 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23283
23284 cx.set_state(indoc! {"
23285 <span>ˇ</span>
23286 "});
23287 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23288 cx.assert_editor_state(indoc! {"
23289 <span>
23290 ˇ
23291 </span>
23292 "});
23293
23294 cx.set_state(indoc! {"
23295 <span><span></span>ˇ</span>
23296 "});
23297 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23298 cx.assert_editor_state(indoc! {"
23299 <span><span></span>
23300 ˇ</span>
23301 "});
23302
23303 cx.set_state(indoc! {"
23304 <span>ˇ
23305 </span>
23306 "});
23307 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23308 cx.assert_editor_state(indoc! {"
23309 <span>
23310 ˇ
23311 </span>
23312 "});
23313}
23314
23315#[gpui::test(iterations = 10)]
23316async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23317 init_test(cx, |_| {});
23318
23319 let fs = FakeFs::new(cx.executor());
23320 fs.insert_tree(
23321 path!("/dir"),
23322 json!({
23323 "a.ts": "a",
23324 }),
23325 )
23326 .await;
23327
23328 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23329 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23330 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23331
23332 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23333 language_registry.add(Arc::new(Language::new(
23334 LanguageConfig {
23335 name: "TypeScript".into(),
23336 matcher: LanguageMatcher {
23337 path_suffixes: vec!["ts".to_string()],
23338 ..Default::default()
23339 },
23340 ..Default::default()
23341 },
23342 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23343 )));
23344 let mut fake_language_servers = language_registry.register_fake_lsp(
23345 "TypeScript",
23346 FakeLspAdapter {
23347 capabilities: lsp::ServerCapabilities {
23348 code_lens_provider: Some(lsp::CodeLensOptions {
23349 resolve_provider: Some(true),
23350 }),
23351 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23352 commands: vec!["_the/command".to_string()],
23353 ..lsp::ExecuteCommandOptions::default()
23354 }),
23355 ..lsp::ServerCapabilities::default()
23356 },
23357 ..FakeLspAdapter::default()
23358 },
23359 );
23360
23361 let editor = workspace
23362 .update(cx, |workspace, window, cx| {
23363 workspace.open_abs_path(
23364 PathBuf::from(path!("/dir/a.ts")),
23365 OpenOptions::default(),
23366 window,
23367 cx,
23368 )
23369 })
23370 .unwrap()
23371 .await
23372 .unwrap()
23373 .downcast::<Editor>()
23374 .unwrap();
23375 cx.executor().run_until_parked();
23376
23377 let fake_server = fake_language_servers.next().await.unwrap();
23378
23379 let buffer = editor.update(cx, |editor, cx| {
23380 editor
23381 .buffer()
23382 .read(cx)
23383 .as_singleton()
23384 .expect("have opened a single file by path")
23385 });
23386
23387 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23388 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23389 drop(buffer_snapshot);
23390 let actions = cx
23391 .update_window(*workspace, |_, window, cx| {
23392 project.code_actions(&buffer, anchor..anchor, window, cx)
23393 })
23394 .unwrap();
23395
23396 fake_server
23397 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23398 Ok(Some(vec![
23399 lsp::CodeLens {
23400 range: lsp::Range::default(),
23401 command: Some(lsp::Command {
23402 title: "Code lens command".to_owned(),
23403 command: "_the/command".to_owned(),
23404 arguments: None,
23405 }),
23406 data: None,
23407 },
23408 lsp::CodeLens {
23409 range: lsp::Range::default(),
23410 command: Some(lsp::Command {
23411 title: "Command not in capabilities".to_owned(),
23412 command: "not in capabilities".to_owned(),
23413 arguments: None,
23414 }),
23415 data: None,
23416 },
23417 lsp::CodeLens {
23418 range: lsp::Range {
23419 start: lsp::Position {
23420 line: 1,
23421 character: 1,
23422 },
23423 end: lsp::Position {
23424 line: 1,
23425 character: 1,
23426 },
23427 },
23428 command: Some(lsp::Command {
23429 title: "Command not in range".to_owned(),
23430 command: "_the/command".to_owned(),
23431 arguments: None,
23432 }),
23433 data: None,
23434 },
23435 ]))
23436 })
23437 .next()
23438 .await;
23439
23440 let actions = actions.await.unwrap();
23441 assert_eq!(
23442 actions.len(),
23443 1,
23444 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23445 );
23446 let action = actions[0].clone();
23447 let apply = project.update(cx, |project, cx| {
23448 project.apply_code_action(buffer.clone(), action, true, cx)
23449 });
23450
23451 // Resolving the code action does not populate its edits. In absence of
23452 // edits, we must execute the given command.
23453 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23454 |mut lens, _| async move {
23455 let lens_command = lens.command.as_mut().expect("should have a command");
23456 assert_eq!(lens_command.title, "Code lens command");
23457 lens_command.arguments = Some(vec![json!("the-argument")]);
23458 Ok(lens)
23459 },
23460 );
23461
23462 // While executing the command, the language server sends the editor
23463 // a `workspaceEdit` request.
23464 fake_server
23465 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23466 let fake = fake_server.clone();
23467 move |params, _| {
23468 assert_eq!(params.command, "_the/command");
23469 let fake = fake.clone();
23470 async move {
23471 fake.server
23472 .request::<lsp::request::ApplyWorkspaceEdit>(
23473 lsp::ApplyWorkspaceEditParams {
23474 label: None,
23475 edit: lsp::WorkspaceEdit {
23476 changes: Some(
23477 [(
23478 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23479 vec![lsp::TextEdit {
23480 range: lsp::Range::new(
23481 lsp::Position::new(0, 0),
23482 lsp::Position::new(0, 0),
23483 ),
23484 new_text: "X".into(),
23485 }],
23486 )]
23487 .into_iter()
23488 .collect(),
23489 ),
23490 ..lsp::WorkspaceEdit::default()
23491 },
23492 },
23493 )
23494 .await
23495 .into_response()
23496 .unwrap();
23497 Ok(Some(json!(null)))
23498 }
23499 }
23500 })
23501 .next()
23502 .await;
23503
23504 // Applying the code lens command returns a project transaction containing the edits
23505 // sent by the language server in its `workspaceEdit` request.
23506 let transaction = apply.await.unwrap();
23507 assert!(transaction.0.contains_key(&buffer));
23508 buffer.update(cx, |buffer, cx| {
23509 assert_eq!(buffer.text(), "Xa");
23510 buffer.undo(cx);
23511 assert_eq!(buffer.text(), "a");
23512 });
23513
23514 let actions_after_edits = cx
23515 .update_window(*workspace, |_, window, cx| {
23516 project.code_actions(&buffer, anchor..anchor, window, cx)
23517 })
23518 .unwrap()
23519 .await
23520 .unwrap();
23521 assert_eq!(
23522 actions, actions_after_edits,
23523 "For the same selection, same code lens actions should be returned"
23524 );
23525
23526 let _responses =
23527 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23528 panic!("No more code lens requests are expected");
23529 });
23530 editor.update_in(cx, |editor, window, cx| {
23531 editor.select_all(&SelectAll, window, cx);
23532 });
23533 cx.executor().run_until_parked();
23534 let new_actions = cx
23535 .update_window(*workspace, |_, window, cx| {
23536 project.code_actions(&buffer, anchor..anchor, window, cx)
23537 })
23538 .unwrap()
23539 .await
23540 .unwrap();
23541 assert_eq!(
23542 actions, new_actions,
23543 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23544 );
23545}
23546
23547#[gpui::test]
23548async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23549 init_test(cx, |_| {});
23550
23551 let fs = FakeFs::new(cx.executor());
23552 let main_text = r#"fn main() {
23553println!("1");
23554println!("2");
23555println!("3");
23556println!("4");
23557println!("5");
23558}"#;
23559 let lib_text = "mod foo {}";
23560 fs.insert_tree(
23561 path!("/a"),
23562 json!({
23563 "lib.rs": lib_text,
23564 "main.rs": main_text,
23565 }),
23566 )
23567 .await;
23568
23569 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23570 let (workspace, cx) =
23571 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23572 let worktree_id = workspace.update(cx, |workspace, cx| {
23573 workspace.project().update(cx, |project, cx| {
23574 project.worktrees(cx).next().unwrap().read(cx).id()
23575 })
23576 });
23577
23578 let expected_ranges = vec![
23579 Point::new(0, 0)..Point::new(0, 0),
23580 Point::new(1, 0)..Point::new(1, 1),
23581 Point::new(2, 0)..Point::new(2, 2),
23582 Point::new(3, 0)..Point::new(3, 3),
23583 ];
23584
23585 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23586 let editor_1 = workspace
23587 .update_in(cx, |workspace, window, cx| {
23588 workspace.open_path(
23589 (worktree_id, rel_path("main.rs")),
23590 Some(pane_1.downgrade()),
23591 true,
23592 window,
23593 cx,
23594 )
23595 })
23596 .unwrap()
23597 .await
23598 .downcast::<Editor>()
23599 .unwrap();
23600 pane_1.update(cx, |pane, cx| {
23601 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23602 open_editor.update(cx, |editor, cx| {
23603 assert_eq!(
23604 editor.display_text(cx),
23605 main_text,
23606 "Original main.rs text on initial open",
23607 );
23608 assert_eq!(
23609 editor
23610 .selections
23611 .all::<Point>(cx)
23612 .into_iter()
23613 .map(|s| s.range())
23614 .collect::<Vec<_>>(),
23615 vec![Point::zero()..Point::zero()],
23616 "Default selections on initial open",
23617 );
23618 })
23619 });
23620 editor_1.update_in(cx, |editor, window, cx| {
23621 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23622 s.select_ranges(expected_ranges.clone());
23623 });
23624 });
23625
23626 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23627 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23628 });
23629 let editor_2 = workspace
23630 .update_in(cx, |workspace, window, cx| {
23631 workspace.open_path(
23632 (worktree_id, rel_path("main.rs")),
23633 Some(pane_2.downgrade()),
23634 true,
23635 window,
23636 cx,
23637 )
23638 })
23639 .unwrap()
23640 .await
23641 .downcast::<Editor>()
23642 .unwrap();
23643 pane_2.update(cx, |pane, cx| {
23644 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23645 open_editor.update(cx, |editor, cx| {
23646 assert_eq!(
23647 editor.display_text(cx),
23648 main_text,
23649 "Original main.rs text on initial open in another panel",
23650 );
23651 assert_eq!(
23652 editor
23653 .selections
23654 .all::<Point>(cx)
23655 .into_iter()
23656 .map(|s| s.range())
23657 .collect::<Vec<_>>(),
23658 vec![Point::zero()..Point::zero()],
23659 "Default selections on initial open in another panel",
23660 );
23661 })
23662 });
23663
23664 editor_2.update_in(cx, |editor, window, cx| {
23665 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23666 });
23667
23668 let _other_editor_1 = workspace
23669 .update_in(cx, |workspace, window, cx| {
23670 workspace.open_path(
23671 (worktree_id, rel_path("lib.rs")),
23672 Some(pane_1.downgrade()),
23673 true,
23674 window,
23675 cx,
23676 )
23677 })
23678 .unwrap()
23679 .await
23680 .downcast::<Editor>()
23681 .unwrap();
23682 pane_1
23683 .update_in(cx, |pane, window, cx| {
23684 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23685 })
23686 .await
23687 .unwrap();
23688 drop(editor_1);
23689 pane_1.update(cx, |pane, cx| {
23690 pane.active_item()
23691 .unwrap()
23692 .downcast::<Editor>()
23693 .unwrap()
23694 .update(cx, |editor, cx| {
23695 assert_eq!(
23696 editor.display_text(cx),
23697 lib_text,
23698 "Other file should be open and active",
23699 );
23700 });
23701 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23702 });
23703
23704 let _other_editor_2 = workspace
23705 .update_in(cx, |workspace, window, cx| {
23706 workspace.open_path(
23707 (worktree_id, rel_path("lib.rs")),
23708 Some(pane_2.downgrade()),
23709 true,
23710 window,
23711 cx,
23712 )
23713 })
23714 .unwrap()
23715 .await
23716 .downcast::<Editor>()
23717 .unwrap();
23718 pane_2
23719 .update_in(cx, |pane, window, cx| {
23720 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23721 })
23722 .await
23723 .unwrap();
23724 drop(editor_2);
23725 pane_2.update(cx, |pane, cx| {
23726 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23727 open_editor.update(cx, |editor, cx| {
23728 assert_eq!(
23729 editor.display_text(cx),
23730 lib_text,
23731 "Other file should be open and active in another panel too",
23732 );
23733 });
23734 assert_eq!(
23735 pane.items().count(),
23736 1,
23737 "No other editors should be open in another pane",
23738 );
23739 });
23740
23741 let _editor_1_reopened = workspace
23742 .update_in(cx, |workspace, window, cx| {
23743 workspace.open_path(
23744 (worktree_id, rel_path("main.rs")),
23745 Some(pane_1.downgrade()),
23746 true,
23747 window,
23748 cx,
23749 )
23750 })
23751 .unwrap()
23752 .await
23753 .downcast::<Editor>()
23754 .unwrap();
23755 let _editor_2_reopened = workspace
23756 .update_in(cx, |workspace, window, cx| {
23757 workspace.open_path(
23758 (worktree_id, rel_path("main.rs")),
23759 Some(pane_2.downgrade()),
23760 true,
23761 window,
23762 cx,
23763 )
23764 })
23765 .unwrap()
23766 .await
23767 .downcast::<Editor>()
23768 .unwrap();
23769 pane_1.update(cx, |pane, cx| {
23770 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23771 open_editor.update(cx, |editor, cx| {
23772 assert_eq!(
23773 editor.display_text(cx),
23774 main_text,
23775 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23776 );
23777 assert_eq!(
23778 editor
23779 .selections
23780 .all::<Point>(cx)
23781 .into_iter()
23782 .map(|s| s.range())
23783 .collect::<Vec<_>>(),
23784 expected_ranges,
23785 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23786 );
23787 })
23788 });
23789 pane_2.update(cx, |pane, cx| {
23790 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23791 open_editor.update(cx, |editor, cx| {
23792 assert_eq!(
23793 editor.display_text(cx),
23794 r#"fn main() {
23795⋯rintln!("1");
23796⋯intln!("2");
23797⋯ntln!("3");
23798println!("4");
23799println!("5");
23800}"#,
23801 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23802 );
23803 assert_eq!(
23804 editor
23805 .selections
23806 .all::<Point>(cx)
23807 .into_iter()
23808 .map(|s| s.range())
23809 .collect::<Vec<_>>(),
23810 vec![Point::zero()..Point::zero()],
23811 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23812 );
23813 })
23814 });
23815}
23816
23817#[gpui::test]
23818async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23819 init_test(cx, |_| {});
23820
23821 let fs = FakeFs::new(cx.executor());
23822 let main_text = r#"fn main() {
23823println!("1");
23824println!("2");
23825println!("3");
23826println!("4");
23827println!("5");
23828}"#;
23829 let lib_text = "mod foo {}";
23830 fs.insert_tree(
23831 path!("/a"),
23832 json!({
23833 "lib.rs": lib_text,
23834 "main.rs": main_text,
23835 }),
23836 )
23837 .await;
23838
23839 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23840 let (workspace, cx) =
23841 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23842 let worktree_id = workspace.update(cx, |workspace, cx| {
23843 workspace.project().update(cx, |project, cx| {
23844 project.worktrees(cx).next().unwrap().read(cx).id()
23845 })
23846 });
23847
23848 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23849 let editor = workspace
23850 .update_in(cx, |workspace, window, cx| {
23851 workspace.open_path(
23852 (worktree_id, rel_path("main.rs")),
23853 Some(pane.downgrade()),
23854 true,
23855 window,
23856 cx,
23857 )
23858 })
23859 .unwrap()
23860 .await
23861 .downcast::<Editor>()
23862 .unwrap();
23863 pane.update(cx, |pane, cx| {
23864 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23865 open_editor.update(cx, |editor, cx| {
23866 assert_eq!(
23867 editor.display_text(cx),
23868 main_text,
23869 "Original main.rs text on initial open",
23870 );
23871 })
23872 });
23873 editor.update_in(cx, |editor, window, cx| {
23874 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23875 });
23876
23877 cx.update_global(|store: &mut SettingsStore, cx| {
23878 store.update_user_settings(cx, |s| {
23879 s.workspace.restore_on_file_reopen = Some(false);
23880 });
23881 });
23882 editor.update_in(cx, |editor, window, cx| {
23883 editor.fold_ranges(
23884 vec![
23885 Point::new(1, 0)..Point::new(1, 1),
23886 Point::new(2, 0)..Point::new(2, 2),
23887 Point::new(3, 0)..Point::new(3, 3),
23888 ],
23889 false,
23890 window,
23891 cx,
23892 );
23893 });
23894 pane.update_in(cx, |pane, window, cx| {
23895 pane.close_all_items(&CloseAllItems::default(), window, cx)
23896 })
23897 .await
23898 .unwrap();
23899 pane.update(cx, |pane, _| {
23900 assert!(pane.active_item().is_none());
23901 });
23902 cx.update_global(|store: &mut SettingsStore, cx| {
23903 store.update_user_settings(cx, |s| {
23904 s.workspace.restore_on_file_reopen = Some(true);
23905 });
23906 });
23907
23908 let _editor_reopened = workspace
23909 .update_in(cx, |workspace, window, cx| {
23910 workspace.open_path(
23911 (worktree_id, rel_path("main.rs")),
23912 Some(pane.downgrade()),
23913 true,
23914 window,
23915 cx,
23916 )
23917 })
23918 .unwrap()
23919 .await
23920 .downcast::<Editor>()
23921 .unwrap();
23922 pane.update(cx, |pane, cx| {
23923 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23924 open_editor.update(cx, |editor, cx| {
23925 assert_eq!(
23926 editor.display_text(cx),
23927 main_text,
23928 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23929 );
23930 })
23931 });
23932}
23933
23934#[gpui::test]
23935async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23936 struct EmptyModalView {
23937 focus_handle: gpui::FocusHandle,
23938 }
23939 impl EventEmitter<DismissEvent> for EmptyModalView {}
23940 impl Render for EmptyModalView {
23941 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23942 div()
23943 }
23944 }
23945 impl Focusable for EmptyModalView {
23946 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23947 self.focus_handle.clone()
23948 }
23949 }
23950 impl workspace::ModalView for EmptyModalView {}
23951 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23952 EmptyModalView {
23953 focus_handle: cx.focus_handle(),
23954 }
23955 }
23956
23957 init_test(cx, |_| {});
23958
23959 let fs = FakeFs::new(cx.executor());
23960 let project = Project::test(fs, [], cx).await;
23961 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23962 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23963 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23964 let editor = cx.new_window_entity(|window, cx| {
23965 Editor::new(
23966 EditorMode::full(),
23967 buffer,
23968 Some(project.clone()),
23969 window,
23970 cx,
23971 )
23972 });
23973 workspace
23974 .update(cx, |workspace, window, cx| {
23975 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23976 })
23977 .unwrap();
23978 editor.update_in(cx, |editor, window, cx| {
23979 editor.open_context_menu(&OpenContextMenu, window, cx);
23980 assert!(editor.mouse_context_menu.is_some());
23981 });
23982 workspace
23983 .update(cx, |workspace, window, cx| {
23984 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23985 })
23986 .unwrap();
23987 cx.read(|cx| {
23988 assert!(editor.read(cx).mouse_context_menu.is_none());
23989 });
23990}
23991
23992fn set_linked_edit_ranges(
23993 opening: (Point, Point),
23994 closing: (Point, Point),
23995 editor: &mut Editor,
23996 cx: &mut Context<Editor>,
23997) {
23998 let Some((buffer, _)) = editor
23999 .buffer
24000 .read(cx)
24001 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24002 else {
24003 panic!("Failed to get buffer for selection position");
24004 };
24005 let buffer = buffer.read(cx);
24006 let buffer_id = buffer.remote_id();
24007 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24008 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24009 let mut linked_ranges = HashMap::default();
24010 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24011 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24012}
24013
24014#[gpui::test]
24015async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24016 init_test(cx, |_| {});
24017
24018 let fs = FakeFs::new(cx.executor());
24019 fs.insert_file(path!("/file.html"), Default::default())
24020 .await;
24021
24022 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24023
24024 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24025 let html_language = Arc::new(Language::new(
24026 LanguageConfig {
24027 name: "HTML".into(),
24028 matcher: LanguageMatcher {
24029 path_suffixes: vec!["html".to_string()],
24030 ..LanguageMatcher::default()
24031 },
24032 brackets: BracketPairConfig {
24033 pairs: vec![BracketPair {
24034 start: "<".into(),
24035 end: ">".into(),
24036 close: true,
24037 ..Default::default()
24038 }],
24039 ..Default::default()
24040 },
24041 ..Default::default()
24042 },
24043 Some(tree_sitter_html::LANGUAGE.into()),
24044 ));
24045 language_registry.add(html_language);
24046 let mut fake_servers = language_registry.register_fake_lsp(
24047 "HTML",
24048 FakeLspAdapter {
24049 capabilities: lsp::ServerCapabilities {
24050 completion_provider: Some(lsp::CompletionOptions {
24051 resolve_provider: Some(true),
24052 ..Default::default()
24053 }),
24054 ..Default::default()
24055 },
24056 ..Default::default()
24057 },
24058 );
24059
24060 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24061 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24062
24063 let worktree_id = workspace
24064 .update(cx, |workspace, _window, cx| {
24065 workspace.project().update(cx, |project, cx| {
24066 project.worktrees(cx).next().unwrap().read(cx).id()
24067 })
24068 })
24069 .unwrap();
24070 project
24071 .update(cx, |project, cx| {
24072 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24073 })
24074 .await
24075 .unwrap();
24076 let editor = workspace
24077 .update(cx, |workspace, window, cx| {
24078 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24079 })
24080 .unwrap()
24081 .await
24082 .unwrap()
24083 .downcast::<Editor>()
24084 .unwrap();
24085
24086 let fake_server = fake_servers.next().await.unwrap();
24087 editor.update_in(cx, |editor, window, cx| {
24088 editor.set_text("<ad></ad>", window, cx);
24089 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24090 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24091 });
24092 set_linked_edit_ranges(
24093 (Point::new(0, 1), Point::new(0, 3)),
24094 (Point::new(0, 6), Point::new(0, 8)),
24095 editor,
24096 cx,
24097 );
24098 });
24099 let mut completion_handle =
24100 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24101 Ok(Some(lsp::CompletionResponse::Array(vec![
24102 lsp::CompletionItem {
24103 label: "head".to_string(),
24104 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24105 lsp::InsertReplaceEdit {
24106 new_text: "head".to_string(),
24107 insert: lsp::Range::new(
24108 lsp::Position::new(0, 1),
24109 lsp::Position::new(0, 3),
24110 ),
24111 replace: lsp::Range::new(
24112 lsp::Position::new(0, 1),
24113 lsp::Position::new(0, 3),
24114 ),
24115 },
24116 )),
24117 ..Default::default()
24118 },
24119 ])))
24120 });
24121 editor.update_in(cx, |editor, window, cx| {
24122 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
24123 });
24124 cx.run_until_parked();
24125 completion_handle.next().await.unwrap();
24126 editor.update(cx, |editor, _| {
24127 assert!(
24128 editor.context_menu_visible(),
24129 "Completion menu should be visible"
24130 );
24131 });
24132 editor.update_in(cx, |editor, window, cx| {
24133 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24134 });
24135 cx.executor().run_until_parked();
24136 editor.update(cx, |editor, cx| {
24137 assert_eq!(editor.text(cx), "<head></head>");
24138 });
24139}
24140
24141#[gpui::test]
24142async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24143 init_test(cx, |_| {});
24144
24145 let mut cx = EditorTestContext::new(cx).await;
24146 let language = Arc::new(Language::new(
24147 LanguageConfig {
24148 name: "TSX".into(),
24149 matcher: LanguageMatcher {
24150 path_suffixes: vec!["tsx".to_string()],
24151 ..LanguageMatcher::default()
24152 },
24153 brackets: BracketPairConfig {
24154 pairs: vec![BracketPair {
24155 start: "<".into(),
24156 end: ">".into(),
24157 close: true,
24158 ..Default::default()
24159 }],
24160 ..Default::default()
24161 },
24162 linked_edit_characters: HashSet::from_iter(['.']),
24163 ..Default::default()
24164 },
24165 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24166 ));
24167 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24168
24169 // Test typing > does not extend linked pair
24170 cx.set_state("<divˇ<div></div>");
24171 cx.update_editor(|editor, _, cx| {
24172 set_linked_edit_ranges(
24173 (Point::new(0, 1), Point::new(0, 4)),
24174 (Point::new(0, 11), Point::new(0, 14)),
24175 editor,
24176 cx,
24177 );
24178 });
24179 cx.update_editor(|editor, window, cx| {
24180 editor.handle_input(">", window, cx);
24181 });
24182 cx.assert_editor_state("<div>ˇ<div></div>");
24183
24184 // Test typing . do extend linked pair
24185 cx.set_state("<Animatedˇ></Animated>");
24186 cx.update_editor(|editor, _, cx| {
24187 set_linked_edit_ranges(
24188 (Point::new(0, 1), Point::new(0, 9)),
24189 (Point::new(0, 12), Point::new(0, 20)),
24190 editor,
24191 cx,
24192 );
24193 });
24194 cx.update_editor(|editor, window, cx| {
24195 editor.handle_input(".", window, cx);
24196 });
24197 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24198 cx.update_editor(|editor, _, cx| {
24199 set_linked_edit_ranges(
24200 (Point::new(0, 1), Point::new(0, 10)),
24201 (Point::new(0, 13), Point::new(0, 21)),
24202 editor,
24203 cx,
24204 );
24205 });
24206 cx.update_editor(|editor, window, cx| {
24207 editor.handle_input("V", window, cx);
24208 });
24209 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24210}
24211
24212#[gpui::test]
24213async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24214 init_test(cx, |_| {});
24215
24216 let fs = FakeFs::new(cx.executor());
24217 fs.insert_tree(
24218 path!("/root"),
24219 json!({
24220 "a": {
24221 "main.rs": "fn main() {}",
24222 },
24223 "foo": {
24224 "bar": {
24225 "external_file.rs": "pub mod external {}",
24226 }
24227 }
24228 }),
24229 )
24230 .await;
24231
24232 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24233 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24234 language_registry.add(rust_lang());
24235 let _fake_servers = language_registry.register_fake_lsp(
24236 "Rust",
24237 FakeLspAdapter {
24238 ..FakeLspAdapter::default()
24239 },
24240 );
24241 let (workspace, cx) =
24242 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24243 let worktree_id = workspace.update(cx, |workspace, cx| {
24244 workspace.project().update(cx, |project, cx| {
24245 project.worktrees(cx).next().unwrap().read(cx).id()
24246 })
24247 });
24248
24249 let assert_language_servers_count =
24250 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24251 project.update(cx, |project, cx| {
24252 let current = project
24253 .lsp_store()
24254 .read(cx)
24255 .as_local()
24256 .unwrap()
24257 .language_servers
24258 .len();
24259 assert_eq!(expected, current, "{context}");
24260 });
24261 };
24262
24263 assert_language_servers_count(
24264 0,
24265 "No servers should be running before any file is open",
24266 cx,
24267 );
24268 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24269 let main_editor = workspace
24270 .update_in(cx, |workspace, window, cx| {
24271 workspace.open_path(
24272 (worktree_id, rel_path("main.rs")),
24273 Some(pane.downgrade()),
24274 true,
24275 window,
24276 cx,
24277 )
24278 })
24279 .unwrap()
24280 .await
24281 .downcast::<Editor>()
24282 .unwrap();
24283 pane.update(cx, |pane, cx| {
24284 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24285 open_editor.update(cx, |editor, cx| {
24286 assert_eq!(
24287 editor.display_text(cx),
24288 "fn main() {}",
24289 "Original main.rs text on initial open",
24290 );
24291 });
24292 assert_eq!(open_editor, main_editor);
24293 });
24294 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24295
24296 let external_editor = workspace
24297 .update_in(cx, |workspace, window, cx| {
24298 workspace.open_abs_path(
24299 PathBuf::from("/root/foo/bar/external_file.rs"),
24300 OpenOptions::default(),
24301 window,
24302 cx,
24303 )
24304 })
24305 .await
24306 .expect("opening external file")
24307 .downcast::<Editor>()
24308 .expect("downcasted external file's open element to editor");
24309 pane.update(cx, |pane, cx| {
24310 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24311 open_editor.update(cx, |editor, cx| {
24312 assert_eq!(
24313 editor.display_text(cx),
24314 "pub mod external {}",
24315 "External file is open now",
24316 );
24317 });
24318 assert_eq!(open_editor, external_editor);
24319 });
24320 assert_language_servers_count(
24321 1,
24322 "Second, external, *.rs file should join the existing server",
24323 cx,
24324 );
24325
24326 pane.update_in(cx, |pane, window, cx| {
24327 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24328 })
24329 .await
24330 .unwrap();
24331 pane.update_in(cx, |pane, window, cx| {
24332 pane.navigate_backward(&Default::default(), window, cx);
24333 });
24334 cx.run_until_parked();
24335 pane.update(cx, |pane, cx| {
24336 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24337 open_editor.update(cx, |editor, cx| {
24338 assert_eq!(
24339 editor.display_text(cx),
24340 "pub mod external {}",
24341 "External file is open now",
24342 );
24343 });
24344 });
24345 assert_language_servers_count(
24346 1,
24347 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24348 cx,
24349 );
24350
24351 cx.update(|_, cx| {
24352 workspace::reload(cx);
24353 });
24354 assert_language_servers_count(
24355 1,
24356 "After reloading the worktree with local and external files opened, only one project should be started",
24357 cx,
24358 );
24359}
24360
24361#[gpui::test]
24362async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24363 init_test(cx, |_| {});
24364
24365 let mut cx = EditorTestContext::new(cx).await;
24366 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24367 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24368
24369 // test cursor move to start of each line on tab
24370 // for `if`, `elif`, `else`, `while`, `with` and `for`
24371 cx.set_state(indoc! {"
24372 def main():
24373 ˇ for item in items:
24374 ˇ while item.active:
24375 ˇ if item.value > 10:
24376 ˇ continue
24377 ˇ elif item.value < 0:
24378 ˇ break
24379 ˇ else:
24380 ˇ with item.context() as ctx:
24381 ˇ yield count
24382 ˇ else:
24383 ˇ log('while else')
24384 ˇ else:
24385 ˇ log('for else')
24386 "});
24387 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24388 cx.assert_editor_state(indoc! {"
24389 def main():
24390 ˇfor item in items:
24391 ˇwhile item.active:
24392 ˇif item.value > 10:
24393 ˇcontinue
24394 ˇelif item.value < 0:
24395 ˇbreak
24396 ˇelse:
24397 ˇwith item.context() as ctx:
24398 ˇyield count
24399 ˇelse:
24400 ˇlog('while else')
24401 ˇelse:
24402 ˇlog('for else')
24403 "});
24404 // test relative indent is preserved when tab
24405 // for `if`, `elif`, `else`, `while`, `with` and `for`
24406 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24407 cx.assert_editor_state(indoc! {"
24408 def main():
24409 ˇfor item in items:
24410 ˇwhile item.active:
24411 ˇif item.value > 10:
24412 ˇcontinue
24413 ˇelif item.value < 0:
24414 ˇbreak
24415 ˇelse:
24416 ˇwith item.context() as ctx:
24417 ˇyield count
24418 ˇelse:
24419 ˇlog('while else')
24420 ˇelse:
24421 ˇlog('for else')
24422 "});
24423
24424 // test cursor move to start of each line on tab
24425 // for `try`, `except`, `else`, `finally`, `match` and `def`
24426 cx.set_state(indoc! {"
24427 def main():
24428 ˇ try:
24429 ˇ fetch()
24430 ˇ except ValueError:
24431 ˇ handle_error()
24432 ˇ else:
24433 ˇ match value:
24434 ˇ case _:
24435 ˇ finally:
24436 ˇ def status():
24437 ˇ return 0
24438 "});
24439 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24440 cx.assert_editor_state(indoc! {"
24441 def main():
24442 ˇtry:
24443 ˇfetch()
24444 ˇexcept ValueError:
24445 ˇhandle_error()
24446 ˇelse:
24447 ˇmatch value:
24448 ˇcase _:
24449 ˇfinally:
24450 ˇdef status():
24451 ˇreturn 0
24452 "});
24453 // test relative indent is preserved when tab
24454 // for `try`, `except`, `else`, `finally`, `match` and `def`
24455 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24456 cx.assert_editor_state(indoc! {"
24457 def main():
24458 ˇtry:
24459 ˇfetch()
24460 ˇexcept ValueError:
24461 ˇhandle_error()
24462 ˇelse:
24463 ˇmatch value:
24464 ˇcase _:
24465 ˇfinally:
24466 ˇdef status():
24467 ˇreturn 0
24468 "});
24469}
24470
24471#[gpui::test]
24472async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24473 init_test(cx, |_| {});
24474
24475 let mut cx = EditorTestContext::new(cx).await;
24476 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24477 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24478
24479 // test `else` auto outdents when typed inside `if` block
24480 cx.set_state(indoc! {"
24481 def main():
24482 if i == 2:
24483 return
24484 ˇ
24485 "});
24486 cx.update_editor(|editor, window, cx| {
24487 editor.handle_input("else:", window, cx);
24488 });
24489 cx.assert_editor_state(indoc! {"
24490 def main():
24491 if i == 2:
24492 return
24493 else:ˇ
24494 "});
24495
24496 // test `except` auto outdents when typed inside `try` block
24497 cx.set_state(indoc! {"
24498 def main():
24499 try:
24500 i = 2
24501 ˇ
24502 "});
24503 cx.update_editor(|editor, window, cx| {
24504 editor.handle_input("except:", window, cx);
24505 });
24506 cx.assert_editor_state(indoc! {"
24507 def main():
24508 try:
24509 i = 2
24510 except:ˇ
24511 "});
24512
24513 // test `else` auto outdents when typed inside `except` block
24514 cx.set_state(indoc! {"
24515 def main():
24516 try:
24517 i = 2
24518 except:
24519 j = 2
24520 ˇ
24521 "});
24522 cx.update_editor(|editor, window, cx| {
24523 editor.handle_input("else:", window, cx);
24524 });
24525 cx.assert_editor_state(indoc! {"
24526 def main():
24527 try:
24528 i = 2
24529 except:
24530 j = 2
24531 else:ˇ
24532 "});
24533
24534 // test `finally` auto outdents when typed inside `else` block
24535 cx.set_state(indoc! {"
24536 def main():
24537 try:
24538 i = 2
24539 except:
24540 j = 2
24541 else:
24542 k = 2
24543 ˇ
24544 "});
24545 cx.update_editor(|editor, window, cx| {
24546 editor.handle_input("finally:", window, cx);
24547 });
24548 cx.assert_editor_state(indoc! {"
24549 def main():
24550 try:
24551 i = 2
24552 except:
24553 j = 2
24554 else:
24555 k = 2
24556 finally:ˇ
24557 "});
24558
24559 // test `else` does not outdents when typed inside `except` block right after for block
24560 cx.set_state(indoc! {"
24561 def main():
24562 try:
24563 i = 2
24564 except:
24565 for i in range(n):
24566 pass
24567 ˇ
24568 "});
24569 cx.update_editor(|editor, window, cx| {
24570 editor.handle_input("else:", window, cx);
24571 });
24572 cx.assert_editor_state(indoc! {"
24573 def main():
24574 try:
24575 i = 2
24576 except:
24577 for i in range(n):
24578 pass
24579 else:ˇ
24580 "});
24581
24582 // test `finally` auto outdents when typed inside `else` block right after for block
24583 cx.set_state(indoc! {"
24584 def main():
24585 try:
24586 i = 2
24587 except:
24588 j = 2
24589 else:
24590 for i in range(n):
24591 pass
24592 ˇ
24593 "});
24594 cx.update_editor(|editor, window, cx| {
24595 editor.handle_input("finally:", window, cx);
24596 });
24597 cx.assert_editor_state(indoc! {"
24598 def main():
24599 try:
24600 i = 2
24601 except:
24602 j = 2
24603 else:
24604 for i in range(n):
24605 pass
24606 finally:ˇ
24607 "});
24608
24609 // test `except` outdents to inner "try" block
24610 cx.set_state(indoc! {"
24611 def main():
24612 try:
24613 i = 2
24614 if i == 2:
24615 try:
24616 i = 3
24617 ˇ
24618 "});
24619 cx.update_editor(|editor, window, cx| {
24620 editor.handle_input("except:", window, cx);
24621 });
24622 cx.assert_editor_state(indoc! {"
24623 def main():
24624 try:
24625 i = 2
24626 if i == 2:
24627 try:
24628 i = 3
24629 except:ˇ
24630 "});
24631
24632 // test `except` outdents to outer "try" block
24633 cx.set_state(indoc! {"
24634 def main():
24635 try:
24636 i = 2
24637 if i == 2:
24638 try:
24639 i = 3
24640 ˇ
24641 "});
24642 cx.update_editor(|editor, window, cx| {
24643 editor.handle_input("except:", window, cx);
24644 });
24645 cx.assert_editor_state(indoc! {"
24646 def main():
24647 try:
24648 i = 2
24649 if i == 2:
24650 try:
24651 i = 3
24652 except:ˇ
24653 "});
24654
24655 // test `else` stays at correct indent when typed after `for` block
24656 cx.set_state(indoc! {"
24657 def main():
24658 for i in range(10):
24659 if i == 3:
24660 break
24661 ˇ
24662 "});
24663 cx.update_editor(|editor, window, cx| {
24664 editor.handle_input("else:", window, cx);
24665 });
24666 cx.assert_editor_state(indoc! {"
24667 def main():
24668 for i in range(10):
24669 if i == 3:
24670 break
24671 else:ˇ
24672 "});
24673
24674 // test does not outdent on typing after line with square brackets
24675 cx.set_state(indoc! {"
24676 def f() -> list[str]:
24677 ˇ
24678 "});
24679 cx.update_editor(|editor, window, cx| {
24680 editor.handle_input("a", window, cx);
24681 });
24682 cx.assert_editor_state(indoc! {"
24683 def f() -> list[str]:
24684 aˇ
24685 "});
24686
24687 // test does not outdent on typing : after case keyword
24688 cx.set_state(indoc! {"
24689 match 1:
24690 caseˇ
24691 "});
24692 cx.update_editor(|editor, window, cx| {
24693 editor.handle_input(":", window, cx);
24694 });
24695 cx.assert_editor_state(indoc! {"
24696 match 1:
24697 case:ˇ
24698 "});
24699}
24700
24701#[gpui::test]
24702async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24703 init_test(cx, |_| {});
24704 update_test_language_settings(cx, |settings| {
24705 settings.defaults.extend_comment_on_newline = Some(false);
24706 });
24707 let mut cx = EditorTestContext::new(cx).await;
24708 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24709 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24710
24711 // test correct indent after newline on comment
24712 cx.set_state(indoc! {"
24713 # COMMENT:ˇ
24714 "});
24715 cx.update_editor(|editor, window, cx| {
24716 editor.newline(&Newline, window, cx);
24717 });
24718 cx.assert_editor_state(indoc! {"
24719 # COMMENT:
24720 ˇ
24721 "});
24722
24723 // test correct indent after newline in brackets
24724 cx.set_state(indoc! {"
24725 {ˇ}
24726 "});
24727 cx.update_editor(|editor, window, cx| {
24728 editor.newline(&Newline, window, cx);
24729 });
24730 cx.run_until_parked();
24731 cx.assert_editor_state(indoc! {"
24732 {
24733 ˇ
24734 }
24735 "});
24736
24737 cx.set_state(indoc! {"
24738 (ˇ)
24739 "});
24740 cx.update_editor(|editor, window, cx| {
24741 editor.newline(&Newline, window, cx);
24742 });
24743 cx.run_until_parked();
24744 cx.assert_editor_state(indoc! {"
24745 (
24746 ˇ
24747 )
24748 "});
24749
24750 // do not indent after empty lists or dictionaries
24751 cx.set_state(indoc! {"
24752 a = []ˇ
24753 "});
24754 cx.update_editor(|editor, window, cx| {
24755 editor.newline(&Newline, window, cx);
24756 });
24757 cx.run_until_parked();
24758 cx.assert_editor_state(indoc! {"
24759 a = []
24760 ˇ
24761 "});
24762}
24763
24764#[gpui::test]
24765async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24766 init_test(cx, |_| {});
24767
24768 let mut cx = EditorTestContext::new(cx).await;
24769 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24770 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24771
24772 // test cursor move to start of each line on tab
24773 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24774 cx.set_state(indoc! {"
24775 function main() {
24776 ˇ for item in $items; do
24777 ˇ while [ -n \"$item\" ]; do
24778 ˇ if [ \"$value\" -gt 10 ]; then
24779 ˇ continue
24780 ˇ elif [ \"$value\" -lt 0 ]; then
24781 ˇ break
24782 ˇ else
24783 ˇ echo \"$item\"
24784 ˇ fi
24785 ˇ done
24786 ˇ done
24787 ˇ}
24788 "});
24789 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24790 cx.assert_editor_state(indoc! {"
24791 function main() {
24792 ˇfor item in $items; do
24793 ˇwhile [ -n \"$item\" ]; do
24794 ˇif [ \"$value\" -gt 10 ]; then
24795 ˇcontinue
24796 ˇelif [ \"$value\" -lt 0 ]; then
24797 ˇbreak
24798 ˇelse
24799 ˇecho \"$item\"
24800 ˇfi
24801 ˇdone
24802 ˇdone
24803 ˇ}
24804 "});
24805 // test relative indent is preserved when tab
24806 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24807 cx.assert_editor_state(indoc! {"
24808 function main() {
24809 ˇfor item in $items; do
24810 ˇwhile [ -n \"$item\" ]; do
24811 ˇif [ \"$value\" -gt 10 ]; then
24812 ˇcontinue
24813 ˇelif [ \"$value\" -lt 0 ]; then
24814 ˇbreak
24815 ˇelse
24816 ˇecho \"$item\"
24817 ˇfi
24818 ˇdone
24819 ˇdone
24820 ˇ}
24821 "});
24822
24823 // test cursor move to start of each line on tab
24824 // for `case` statement with patterns
24825 cx.set_state(indoc! {"
24826 function handle() {
24827 ˇ case \"$1\" in
24828 ˇ start)
24829 ˇ echo \"a\"
24830 ˇ ;;
24831 ˇ stop)
24832 ˇ echo \"b\"
24833 ˇ ;;
24834 ˇ *)
24835 ˇ echo \"c\"
24836 ˇ ;;
24837 ˇ esac
24838 ˇ}
24839 "});
24840 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24841 cx.assert_editor_state(indoc! {"
24842 function handle() {
24843 ˇcase \"$1\" in
24844 ˇstart)
24845 ˇecho \"a\"
24846 ˇ;;
24847 ˇstop)
24848 ˇecho \"b\"
24849 ˇ;;
24850 ˇ*)
24851 ˇecho \"c\"
24852 ˇ;;
24853 ˇesac
24854 ˇ}
24855 "});
24856}
24857
24858#[gpui::test]
24859async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24860 init_test(cx, |_| {});
24861
24862 let mut cx = EditorTestContext::new(cx).await;
24863 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24864 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24865
24866 // test indents on comment insert
24867 cx.set_state(indoc! {"
24868 function main() {
24869 ˇ for item in $items; do
24870 ˇ while [ -n \"$item\" ]; do
24871 ˇ if [ \"$value\" -gt 10 ]; then
24872 ˇ continue
24873 ˇ elif [ \"$value\" -lt 0 ]; then
24874 ˇ break
24875 ˇ else
24876 ˇ echo \"$item\"
24877 ˇ fi
24878 ˇ done
24879 ˇ done
24880 ˇ}
24881 "});
24882 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24883 cx.assert_editor_state(indoc! {"
24884 function main() {
24885 #ˇ for item in $items; do
24886 #ˇ while [ -n \"$item\" ]; do
24887 #ˇ if [ \"$value\" -gt 10 ]; then
24888 #ˇ continue
24889 #ˇ elif [ \"$value\" -lt 0 ]; then
24890 #ˇ break
24891 #ˇ else
24892 #ˇ echo \"$item\"
24893 #ˇ fi
24894 #ˇ done
24895 #ˇ done
24896 #ˇ}
24897 "});
24898}
24899
24900#[gpui::test]
24901async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24902 init_test(cx, |_| {});
24903
24904 let mut cx = EditorTestContext::new(cx).await;
24905 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24906 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24907
24908 // test `else` auto outdents when typed inside `if` block
24909 cx.set_state(indoc! {"
24910 if [ \"$1\" = \"test\" ]; then
24911 echo \"foo bar\"
24912 ˇ
24913 "});
24914 cx.update_editor(|editor, window, cx| {
24915 editor.handle_input("else", window, cx);
24916 });
24917 cx.assert_editor_state(indoc! {"
24918 if [ \"$1\" = \"test\" ]; then
24919 echo \"foo bar\"
24920 elseˇ
24921 "});
24922
24923 // test `elif` auto outdents when typed inside `if` block
24924 cx.set_state(indoc! {"
24925 if [ \"$1\" = \"test\" ]; then
24926 echo \"foo bar\"
24927 ˇ
24928 "});
24929 cx.update_editor(|editor, window, cx| {
24930 editor.handle_input("elif", window, cx);
24931 });
24932 cx.assert_editor_state(indoc! {"
24933 if [ \"$1\" = \"test\" ]; then
24934 echo \"foo bar\"
24935 elifˇ
24936 "});
24937
24938 // test `fi` auto outdents when typed inside `else` block
24939 cx.set_state(indoc! {"
24940 if [ \"$1\" = \"test\" ]; then
24941 echo \"foo bar\"
24942 else
24943 echo \"bar baz\"
24944 ˇ
24945 "});
24946 cx.update_editor(|editor, window, cx| {
24947 editor.handle_input("fi", window, cx);
24948 });
24949 cx.assert_editor_state(indoc! {"
24950 if [ \"$1\" = \"test\" ]; then
24951 echo \"foo bar\"
24952 else
24953 echo \"bar baz\"
24954 fiˇ
24955 "});
24956
24957 // test `done` auto outdents when typed inside `while` block
24958 cx.set_state(indoc! {"
24959 while read line; do
24960 echo \"$line\"
24961 ˇ
24962 "});
24963 cx.update_editor(|editor, window, cx| {
24964 editor.handle_input("done", window, cx);
24965 });
24966 cx.assert_editor_state(indoc! {"
24967 while read line; do
24968 echo \"$line\"
24969 doneˇ
24970 "});
24971
24972 // test `done` auto outdents when typed inside `for` block
24973 cx.set_state(indoc! {"
24974 for file in *.txt; do
24975 cat \"$file\"
24976 ˇ
24977 "});
24978 cx.update_editor(|editor, window, cx| {
24979 editor.handle_input("done", window, cx);
24980 });
24981 cx.assert_editor_state(indoc! {"
24982 for file in *.txt; do
24983 cat \"$file\"
24984 doneˇ
24985 "});
24986
24987 // test `esac` auto outdents when typed inside `case` block
24988 cx.set_state(indoc! {"
24989 case \"$1\" in
24990 start)
24991 echo \"foo bar\"
24992 ;;
24993 stop)
24994 echo \"bar baz\"
24995 ;;
24996 ˇ
24997 "});
24998 cx.update_editor(|editor, window, cx| {
24999 editor.handle_input("esac", window, cx);
25000 });
25001 cx.assert_editor_state(indoc! {"
25002 case \"$1\" in
25003 start)
25004 echo \"foo bar\"
25005 ;;
25006 stop)
25007 echo \"bar baz\"
25008 ;;
25009 esacˇ
25010 "});
25011
25012 // test `*)` auto outdents when typed inside `case` block
25013 cx.set_state(indoc! {"
25014 case \"$1\" in
25015 start)
25016 echo \"foo bar\"
25017 ;;
25018 ˇ
25019 "});
25020 cx.update_editor(|editor, window, cx| {
25021 editor.handle_input("*)", window, cx);
25022 });
25023 cx.assert_editor_state(indoc! {"
25024 case \"$1\" in
25025 start)
25026 echo \"foo bar\"
25027 ;;
25028 *)ˇ
25029 "});
25030
25031 // test `fi` outdents to correct level with nested if blocks
25032 cx.set_state(indoc! {"
25033 if [ \"$1\" = \"test\" ]; then
25034 echo \"outer if\"
25035 if [ \"$2\" = \"debug\" ]; then
25036 echo \"inner if\"
25037 ˇ
25038 "});
25039 cx.update_editor(|editor, window, cx| {
25040 editor.handle_input("fi", window, cx);
25041 });
25042 cx.assert_editor_state(indoc! {"
25043 if [ \"$1\" = \"test\" ]; then
25044 echo \"outer if\"
25045 if [ \"$2\" = \"debug\" ]; then
25046 echo \"inner if\"
25047 fiˇ
25048 "});
25049}
25050
25051#[gpui::test]
25052async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25053 init_test(cx, |_| {});
25054 update_test_language_settings(cx, |settings| {
25055 settings.defaults.extend_comment_on_newline = Some(false);
25056 });
25057 let mut cx = EditorTestContext::new(cx).await;
25058 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25059 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25060
25061 // test correct indent after newline on comment
25062 cx.set_state(indoc! {"
25063 # COMMENT:ˇ
25064 "});
25065 cx.update_editor(|editor, window, cx| {
25066 editor.newline(&Newline, window, cx);
25067 });
25068 cx.assert_editor_state(indoc! {"
25069 # COMMENT:
25070 ˇ
25071 "});
25072
25073 // test correct indent after newline after `then`
25074 cx.set_state(indoc! {"
25075
25076 if [ \"$1\" = \"test\" ]; thenˇ
25077 "});
25078 cx.update_editor(|editor, window, cx| {
25079 editor.newline(&Newline, window, cx);
25080 });
25081 cx.run_until_parked();
25082 cx.assert_editor_state(indoc! {"
25083
25084 if [ \"$1\" = \"test\" ]; then
25085 ˇ
25086 "});
25087
25088 // test correct indent after newline after `else`
25089 cx.set_state(indoc! {"
25090 if [ \"$1\" = \"test\" ]; then
25091 elseˇ
25092 "});
25093 cx.update_editor(|editor, window, cx| {
25094 editor.newline(&Newline, window, cx);
25095 });
25096 cx.run_until_parked();
25097 cx.assert_editor_state(indoc! {"
25098 if [ \"$1\" = \"test\" ]; then
25099 else
25100 ˇ
25101 "});
25102
25103 // test correct indent after newline after `elif`
25104 cx.set_state(indoc! {"
25105 if [ \"$1\" = \"test\" ]; then
25106 elifˇ
25107 "});
25108 cx.update_editor(|editor, window, cx| {
25109 editor.newline(&Newline, window, cx);
25110 });
25111 cx.run_until_parked();
25112 cx.assert_editor_state(indoc! {"
25113 if [ \"$1\" = \"test\" ]; then
25114 elif
25115 ˇ
25116 "});
25117
25118 // test correct indent after newline after `do`
25119 cx.set_state(indoc! {"
25120 for file in *.txt; doˇ
25121 "});
25122 cx.update_editor(|editor, window, cx| {
25123 editor.newline(&Newline, window, cx);
25124 });
25125 cx.run_until_parked();
25126 cx.assert_editor_state(indoc! {"
25127 for file in *.txt; do
25128 ˇ
25129 "});
25130
25131 // test correct indent after newline after case pattern
25132 cx.set_state(indoc! {"
25133 case \"$1\" in
25134 start)ˇ
25135 "});
25136 cx.update_editor(|editor, window, cx| {
25137 editor.newline(&Newline, window, cx);
25138 });
25139 cx.run_until_parked();
25140 cx.assert_editor_state(indoc! {"
25141 case \"$1\" in
25142 start)
25143 ˇ
25144 "});
25145
25146 // test correct indent after newline after case pattern
25147 cx.set_state(indoc! {"
25148 case \"$1\" in
25149 start)
25150 ;;
25151 *)ˇ
25152 "});
25153 cx.update_editor(|editor, window, cx| {
25154 editor.newline(&Newline, window, cx);
25155 });
25156 cx.run_until_parked();
25157 cx.assert_editor_state(indoc! {"
25158 case \"$1\" in
25159 start)
25160 ;;
25161 *)
25162 ˇ
25163 "});
25164
25165 // test correct indent after newline after function opening brace
25166 cx.set_state(indoc! {"
25167 function test() {ˇ}
25168 "});
25169 cx.update_editor(|editor, window, cx| {
25170 editor.newline(&Newline, window, cx);
25171 });
25172 cx.run_until_parked();
25173 cx.assert_editor_state(indoc! {"
25174 function test() {
25175 ˇ
25176 }
25177 "});
25178
25179 // test no extra indent after semicolon on same line
25180 cx.set_state(indoc! {"
25181 echo \"test\";ˇ
25182 "});
25183 cx.update_editor(|editor, window, cx| {
25184 editor.newline(&Newline, window, cx);
25185 });
25186 cx.run_until_parked();
25187 cx.assert_editor_state(indoc! {"
25188 echo \"test\";
25189 ˇ
25190 "});
25191}
25192
25193fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25194 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25195 point..point
25196}
25197
25198#[track_caller]
25199fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25200 let (text, ranges) = marked_text_ranges(marked_text, true);
25201 assert_eq!(editor.text(cx), text);
25202 assert_eq!(
25203 editor.selections.ranges(cx),
25204 ranges,
25205 "Assert selections are {}",
25206 marked_text
25207 );
25208}
25209
25210pub fn handle_signature_help_request(
25211 cx: &mut EditorLspTestContext,
25212 mocked_response: lsp::SignatureHelp,
25213) -> impl Future<Output = ()> + use<> {
25214 let mut request =
25215 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25216 let mocked_response = mocked_response.clone();
25217 async move { Ok(Some(mocked_response)) }
25218 });
25219
25220 async move {
25221 request.next().await;
25222 }
25223}
25224
25225#[track_caller]
25226pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25227 cx.update_editor(|editor, _, _| {
25228 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25229 let entries = menu.entries.borrow();
25230 let entries = entries
25231 .iter()
25232 .map(|entry| entry.string.as_str())
25233 .collect::<Vec<_>>();
25234 assert_eq!(entries, expected);
25235 } else {
25236 panic!("Expected completions menu");
25237 }
25238 });
25239}
25240
25241/// Handle completion request passing a marked string specifying where the completion
25242/// should be triggered from using '|' character, what range should be replaced, and what completions
25243/// should be returned using '<' and '>' to delimit the range.
25244///
25245/// Also see `handle_completion_request_with_insert_and_replace`.
25246#[track_caller]
25247pub fn handle_completion_request(
25248 marked_string: &str,
25249 completions: Vec<&'static str>,
25250 is_incomplete: bool,
25251 counter: Arc<AtomicUsize>,
25252 cx: &mut EditorLspTestContext,
25253) -> impl Future<Output = ()> {
25254 let complete_from_marker: TextRangeMarker = '|'.into();
25255 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25256 let (_, mut marked_ranges) = marked_text_ranges_by(
25257 marked_string,
25258 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25259 );
25260
25261 let complete_from_position =
25262 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25263 let replace_range =
25264 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25265
25266 let mut request =
25267 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25268 let completions = completions.clone();
25269 counter.fetch_add(1, atomic::Ordering::Release);
25270 async move {
25271 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25272 assert_eq!(
25273 params.text_document_position.position,
25274 complete_from_position
25275 );
25276 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25277 is_incomplete,
25278 item_defaults: None,
25279 items: completions
25280 .iter()
25281 .map(|completion_text| lsp::CompletionItem {
25282 label: completion_text.to_string(),
25283 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25284 range: replace_range,
25285 new_text: completion_text.to_string(),
25286 })),
25287 ..Default::default()
25288 })
25289 .collect(),
25290 })))
25291 }
25292 });
25293
25294 async move {
25295 request.next().await;
25296 }
25297}
25298
25299/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25300/// given instead, which also contains an `insert` range.
25301///
25302/// This function uses markers to define ranges:
25303/// - `|` marks the cursor position
25304/// - `<>` marks the replace range
25305/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25306pub fn handle_completion_request_with_insert_and_replace(
25307 cx: &mut EditorLspTestContext,
25308 marked_string: &str,
25309 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25310 counter: Arc<AtomicUsize>,
25311) -> impl Future<Output = ()> {
25312 let complete_from_marker: TextRangeMarker = '|'.into();
25313 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25314 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25315
25316 let (_, mut marked_ranges) = marked_text_ranges_by(
25317 marked_string,
25318 vec![
25319 complete_from_marker.clone(),
25320 replace_range_marker.clone(),
25321 insert_range_marker.clone(),
25322 ],
25323 );
25324
25325 let complete_from_position =
25326 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25327 let replace_range =
25328 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25329
25330 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25331 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25332 _ => lsp::Range {
25333 start: replace_range.start,
25334 end: complete_from_position,
25335 },
25336 };
25337
25338 let mut request =
25339 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25340 let completions = completions.clone();
25341 counter.fetch_add(1, atomic::Ordering::Release);
25342 async move {
25343 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25344 assert_eq!(
25345 params.text_document_position.position, complete_from_position,
25346 "marker `|` position doesn't match",
25347 );
25348 Ok(Some(lsp::CompletionResponse::Array(
25349 completions
25350 .iter()
25351 .map(|(label, new_text)| lsp::CompletionItem {
25352 label: label.to_string(),
25353 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25354 lsp::InsertReplaceEdit {
25355 insert: insert_range,
25356 replace: replace_range,
25357 new_text: new_text.to_string(),
25358 },
25359 )),
25360 ..Default::default()
25361 })
25362 .collect(),
25363 )))
25364 }
25365 });
25366
25367 async move {
25368 request.next().await;
25369 }
25370}
25371
25372fn handle_resolve_completion_request(
25373 cx: &mut EditorLspTestContext,
25374 edits: Option<Vec<(&'static str, &'static str)>>,
25375) -> impl Future<Output = ()> {
25376 let edits = edits.map(|edits| {
25377 edits
25378 .iter()
25379 .map(|(marked_string, new_text)| {
25380 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25381 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25382 lsp::TextEdit::new(replace_range, new_text.to_string())
25383 })
25384 .collect::<Vec<_>>()
25385 });
25386
25387 let mut request =
25388 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25389 let edits = edits.clone();
25390 async move {
25391 Ok(lsp::CompletionItem {
25392 additional_text_edits: edits,
25393 ..Default::default()
25394 })
25395 }
25396 });
25397
25398 async move {
25399 request.next().await;
25400 }
25401}
25402
25403pub(crate) fn update_test_language_settings(
25404 cx: &mut TestAppContext,
25405 f: impl Fn(&mut AllLanguageSettingsContent),
25406) {
25407 cx.update(|cx| {
25408 SettingsStore::update_global(cx, |store, cx| {
25409 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25410 });
25411 });
25412}
25413
25414pub(crate) fn update_test_project_settings(
25415 cx: &mut TestAppContext,
25416 f: impl Fn(&mut ProjectSettingsContent),
25417) {
25418 cx.update(|cx| {
25419 SettingsStore::update_global(cx, |store, cx| {
25420 store.update_user_settings(cx, |settings| f(&mut settings.project));
25421 });
25422 });
25423}
25424
25425pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25426 cx.update(|cx| {
25427 assets::Assets.load_test_fonts(cx);
25428 let store = SettingsStore::test(cx);
25429 cx.set_global(store);
25430 theme::init(theme::LoadThemes::JustBase, cx);
25431 release_channel::init(SemanticVersion::default(), cx);
25432 client::init_settings(cx);
25433 language::init(cx);
25434 Project::init_settings(cx);
25435 workspace::init_settings(cx);
25436 crate::init(cx);
25437 });
25438 zlog::init_test();
25439 update_test_language_settings(cx, f);
25440}
25441
25442#[track_caller]
25443fn assert_hunk_revert(
25444 not_reverted_text_with_selections: &str,
25445 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25446 expected_reverted_text_with_selections: &str,
25447 base_text: &str,
25448 cx: &mut EditorLspTestContext,
25449) {
25450 cx.set_state(not_reverted_text_with_selections);
25451 cx.set_head_text(base_text);
25452 cx.executor().run_until_parked();
25453
25454 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25455 let snapshot = editor.snapshot(window, cx);
25456 let reverted_hunk_statuses = snapshot
25457 .buffer_snapshot()
25458 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
25459 .map(|hunk| hunk.status().kind)
25460 .collect::<Vec<_>>();
25461
25462 editor.git_restore(&Default::default(), window, cx);
25463 reverted_hunk_statuses
25464 });
25465 cx.executor().run_until_parked();
25466 cx.assert_editor_state(expected_reverted_text_with_selections);
25467 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25468}
25469
25470#[gpui::test(iterations = 10)]
25471async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25472 init_test(cx, |_| {});
25473
25474 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25475 let counter = diagnostic_requests.clone();
25476
25477 let fs = FakeFs::new(cx.executor());
25478 fs.insert_tree(
25479 path!("/a"),
25480 json!({
25481 "first.rs": "fn main() { let a = 5; }",
25482 "second.rs": "// Test file",
25483 }),
25484 )
25485 .await;
25486
25487 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25488 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25489 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25490
25491 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25492 language_registry.add(rust_lang());
25493 let mut fake_servers = language_registry.register_fake_lsp(
25494 "Rust",
25495 FakeLspAdapter {
25496 capabilities: lsp::ServerCapabilities {
25497 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25498 lsp::DiagnosticOptions {
25499 identifier: None,
25500 inter_file_dependencies: true,
25501 workspace_diagnostics: true,
25502 work_done_progress_options: Default::default(),
25503 },
25504 )),
25505 ..Default::default()
25506 },
25507 ..Default::default()
25508 },
25509 );
25510
25511 let editor = workspace
25512 .update(cx, |workspace, window, cx| {
25513 workspace.open_abs_path(
25514 PathBuf::from(path!("/a/first.rs")),
25515 OpenOptions::default(),
25516 window,
25517 cx,
25518 )
25519 })
25520 .unwrap()
25521 .await
25522 .unwrap()
25523 .downcast::<Editor>()
25524 .unwrap();
25525 let fake_server = fake_servers.next().await.unwrap();
25526 let server_id = fake_server.server.server_id();
25527 let mut first_request = fake_server
25528 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25529 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25530 let result_id = Some(new_result_id.to_string());
25531 assert_eq!(
25532 params.text_document.uri,
25533 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25534 );
25535 async move {
25536 Ok(lsp::DocumentDiagnosticReportResult::Report(
25537 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25538 related_documents: None,
25539 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25540 items: Vec::new(),
25541 result_id,
25542 },
25543 }),
25544 ))
25545 }
25546 });
25547
25548 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25549 project.update(cx, |project, cx| {
25550 let buffer_id = editor
25551 .read(cx)
25552 .buffer()
25553 .read(cx)
25554 .as_singleton()
25555 .expect("created a singleton buffer")
25556 .read(cx)
25557 .remote_id();
25558 let buffer_result_id = project
25559 .lsp_store()
25560 .read(cx)
25561 .result_id(server_id, buffer_id, cx);
25562 assert_eq!(expected, buffer_result_id);
25563 });
25564 };
25565
25566 ensure_result_id(None, cx);
25567 cx.executor().advance_clock(Duration::from_millis(60));
25568 cx.executor().run_until_parked();
25569 assert_eq!(
25570 diagnostic_requests.load(atomic::Ordering::Acquire),
25571 1,
25572 "Opening file should trigger diagnostic request"
25573 );
25574 first_request
25575 .next()
25576 .await
25577 .expect("should have sent the first diagnostics pull request");
25578 ensure_result_id(Some("1".to_string()), cx);
25579
25580 // Editing should trigger diagnostics
25581 editor.update_in(cx, |editor, window, cx| {
25582 editor.handle_input("2", window, cx)
25583 });
25584 cx.executor().advance_clock(Duration::from_millis(60));
25585 cx.executor().run_until_parked();
25586 assert_eq!(
25587 diagnostic_requests.load(atomic::Ordering::Acquire),
25588 2,
25589 "Editing should trigger diagnostic request"
25590 );
25591 ensure_result_id(Some("2".to_string()), cx);
25592
25593 // Moving cursor should not trigger diagnostic request
25594 editor.update_in(cx, |editor, window, cx| {
25595 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25596 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25597 });
25598 });
25599 cx.executor().advance_clock(Duration::from_millis(60));
25600 cx.executor().run_until_parked();
25601 assert_eq!(
25602 diagnostic_requests.load(atomic::Ordering::Acquire),
25603 2,
25604 "Cursor movement should not trigger diagnostic request"
25605 );
25606 ensure_result_id(Some("2".to_string()), cx);
25607 // Multiple rapid edits should be debounced
25608 for _ in 0..5 {
25609 editor.update_in(cx, |editor, window, cx| {
25610 editor.handle_input("x", window, cx)
25611 });
25612 }
25613 cx.executor().advance_clock(Duration::from_millis(60));
25614 cx.executor().run_until_parked();
25615
25616 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25617 assert!(
25618 final_requests <= 4,
25619 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25620 );
25621 ensure_result_id(Some(final_requests.to_string()), cx);
25622}
25623
25624#[gpui::test]
25625async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25626 // Regression test for issue #11671
25627 // Previously, adding a cursor after moving multiple cursors would reset
25628 // the cursor count instead of adding to the existing cursors.
25629 init_test(cx, |_| {});
25630 let mut cx = EditorTestContext::new(cx).await;
25631
25632 // Create a simple buffer with cursor at start
25633 cx.set_state(indoc! {"
25634 ˇaaaa
25635 bbbb
25636 cccc
25637 dddd
25638 eeee
25639 ffff
25640 gggg
25641 hhhh"});
25642
25643 // Add 2 cursors below (so we have 3 total)
25644 cx.update_editor(|editor, window, cx| {
25645 editor.add_selection_below(&Default::default(), window, cx);
25646 editor.add_selection_below(&Default::default(), window, cx);
25647 });
25648
25649 // Verify we have 3 cursors
25650 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25651 assert_eq!(
25652 initial_count, 3,
25653 "Should have 3 cursors after adding 2 below"
25654 );
25655
25656 // Move down one line
25657 cx.update_editor(|editor, window, cx| {
25658 editor.move_down(&MoveDown, window, cx);
25659 });
25660
25661 // Add another cursor below
25662 cx.update_editor(|editor, window, cx| {
25663 editor.add_selection_below(&Default::default(), window, cx);
25664 });
25665
25666 // Should now have 4 cursors (3 original + 1 new)
25667 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25668 assert_eq!(
25669 final_count, 4,
25670 "Should have 4 cursors after moving and adding another"
25671 );
25672}
25673
25674#[gpui::test(iterations = 10)]
25675async fn test_document_colors(cx: &mut TestAppContext) {
25676 let expected_color = Rgba {
25677 r: 0.33,
25678 g: 0.33,
25679 b: 0.33,
25680 a: 0.33,
25681 };
25682
25683 init_test(cx, |_| {});
25684
25685 let fs = FakeFs::new(cx.executor());
25686 fs.insert_tree(
25687 path!("/a"),
25688 json!({
25689 "first.rs": "fn main() { let a = 5; }",
25690 }),
25691 )
25692 .await;
25693
25694 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25695 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25696 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25697
25698 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25699 language_registry.add(rust_lang());
25700 let mut fake_servers = language_registry.register_fake_lsp(
25701 "Rust",
25702 FakeLspAdapter {
25703 capabilities: lsp::ServerCapabilities {
25704 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25705 ..lsp::ServerCapabilities::default()
25706 },
25707 name: "rust-analyzer",
25708 ..FakeLspAdapter::default()
25709 },
25710 );
25711 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25712 "Rust",
25713 FakeLspAdapter {
25714 capabilities: lsp::ServerCapabilities {
25715 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25716 ..lsp::ServerCapabilities::default()
25717 },
25718 name: "not-rust-analyzer",
25719 ..FakeLspAdapter::default()
25720 },
25721 );
25722
25723 let editor = workspace
25724 .update(cx, |workspace, window, cx| {
25725 workspace.open_abs_path(
25726 PathBuf::from(path!("/a/first.rs")),
25727 OpenOptions::default(),
25728 window,
25729 cx,
25730 )
25731 })
25732 .unwrap()
25733 .await
25734 .unwrap()
25735 .downcast::<Editor>()
25736 .unwrap();
25737 let fake_language_server = fake_servers.next().await.unwrap();
25738 let fake_language_server_without_capabilities =
25739 fake_servers_without_capabilities.next().await.unwrap();
25740 let requests_made = Arc::new(AtomicUsize::new(0));
25741 let closure_requests_made = Arc::clone(&requests_made);
25742 let mut color_request_handle = fake_language_server
25743 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25744 let requests_made = Arc::clone(&closure_requests_made);
25745 async move {
25746 assert_eq!(
25747 params.text_document.uri,
25748 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25749 );
25750 requests_made.fetch_add(1, atomic::Ordering::Release);
25751 Ok(vec![
25752 lsp::ColorInformation {
25753 range: lsp::Range {
25754 start: lsp::Position {
25755 line: 0,
25756 character: 0,
25757 },
25758 end: lsp::Position {
25759 line: 0,
25760 character: 1,
25761 },
25762 },
25763 color: lsp::Color {
25764 red: 0.33,
25765 green: 0.33,
25766 blue: 0.33,
25767 alpha: 0.33,
25768 },
25769 },
25770 lsp::ColorInformation {
25771 range: lsp::Range {
25772 start: lsp::Position {
25773 line: 0,
25774 character: 0,
25775 },
25776 end: lsp::Position {
25777 line: 0,
25778 character: 1,
25779 },
25780 },
25781 color: lsp::Color {
25782 red: 0.33,
25783 green: 0.33,
25784 blue: 0.33,
25785 alpha: 0.33,
25786 },
25787 },
25788 ])
25789 }
25790 });
25791
25792 let _handle = fake_language_server_without_capabilities
25793 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25794 panic!("Should not be called");
25795 });
25796 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
25797 color_request_handle.next().await.unwrap();
25798 cx.run_until_parked();
25799 assert_eq!(
25800 1,
25801 requests_made.load(atomic::Ordering::Acquire),
25802 "Should query for colors once per editor open"
25803 );
25804 editor.update_in(cx, |editor, _, cx| {
25805 assert_eq!(
25806 vec![expected_color],
25807 extract_color_inlays(editor, cx),
25808 "Should have an initial inlay"
25809 );
25810 });
25811
25812 // opening another file in a split should not influence the LSP query counter
25813 workspace
25814 .update(cx, |workspace, window, cx| {
25815 assert_eq!(
25816 workspace.panes().len(),
25817 1,
25818 "Should have one pane with one editor"
25819 );
25820 workspace.move_item_to_pane_in_direction(
25821 &MoveItemToPaneInDirection {
25822 direction: SplitDirection::Right,
25823 focus: false,
25824 clone: true,
25825 },
25826 window,
25827 cx,
25828 );
25829 })
25830 .unwrap();
25831 cx.run_until_parked();
25832 workspace
25833 .update(cx, |workspace, _, cx| {
25834 let panes = workspace.panes();
25835 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25836 for pane in panes {
25837 let editor = pane
25838 .read(cx)
25839 .active_item()
25840 .and_then(|item| item.downcast::<Editor>())
25841 .expect("Should have opened an editor in each split");
25842 let editor_file = editor
25843 .read(cx)
25844 .buffer()
25845 .read(cx)
25846 .as_singleton()
25847 .expect("test deals with singleton buffers")
25848 .read(cx)
25849 .file()
25850 .expect("test buffese should have a file")
25851 .path();
25852 assert_eq!(
25853 editor_file.as_ref(),
25854 rel_path("first.rs"),
25855 "Both editors should be opened for the same file"
25856 )
25857 }
25858 })
25859 .unwrap();
25860
25861 cx.executor().advance_clock(Duration::from_millis(500));
25862 let save = editor.update_in(cx, |editor, window, cx| {
25863 editor.move_to_end(&MoveToEnd, window, cx);
25864 editor.handle_input("dirty", window, cx);
25865 editor.save(
25866 SaveOptions {
25867 format: true,
25868 autosave: true,
25869 },
25870 project.clone(),
25871 window,
25872 cx,
25873 )
25874 });
25875 save.await.unwrap();
25876
25877 color_request_handle.next().await.unwrap();
25878 cx.run_until_parked();
25879 assert_eq!(
25880 2,
25881 requests_made.load(atomic::Ordering::Acquire),
25882 "Should query for colors once per save (deduplicated) and once per formatting after save"
25883 );
25884
25885 drop(editor);
25886 let close = workspace
25887 .update(cx, |workspace, window, cx| {
25888 workspace.active_pane().update(cx, |pane, cx| {
25889 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25890 })
25891 })
25892 .unwrap();
25893 close.await.unwrap();
25894 let close = workspace
25895 .update(cx, |workspace, window, cx| {
25896 workspace.active_pane().update(cx, |pane, cx| {
25897 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25898 })
25899 })
25900 .unwrap();
25901 close.await.unwrap();
25902 assert_eq!(
25903 2,
25904 requests_made.load(atomic::Ordering::Acquire),
25905 "After saving and closing all editors, no extra requests should be made"
25906 );
25907 workspace
25908 .update(cx, |workspace, _, cx| {
25909 assert!(
25910 workspace.active_item(cx).is_none(),
25911 "Should close all editors"
25912 )
25913 })
25914 .unwrap();
25915
25916 workspace
25917 .update(cx, |workspace, window, cx| {
25918 workspace.active_pane().update(cx, |pane, cx| {
25919 pane.navigate_backward(&workspace::GoBack, window, cx);
25920 })
25921 })
25922 .unwrap();
25923 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
25924 cx.run_until_parked();
25925 let editor = workspace
25926 .update(cx, |workspace, _, cx| {
25927 workspace
25928 .active_item(cx)
25929 .expect("Should have reopened the editor again after navigating back")
25930 .downcast::<Editor>()
25931 .expect("Should be an editor")
25932 })
25933 .unwrap();
25934
25935 assert_eq!(
25936 2,
25937 requests_made.load(atomic::Ordering::Acquire),
25938 "Cache should be reused on buffer close and reopen"
25939 );
25940 editor.update(cx, |editor, cx| {
25941 assert_eq!(
25942 vec![expected_color],
25943 extract_color_inlays(editor, cx),
25944 "Should have an initial inlay"
25945 );
25946 });
25947
25948 drop(color_request_handle);
25949 let closure_requests_made = Arc::clone(&requests_made);
25950 let mut empty_color_request_handle = fake_language_server
25951 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25952 let requests_made = Arc::clone(&closure_requests_made);
25953 async move {
25954 assert_eq!(
25955 params.text_document.uri,
25956 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25957 );
25958 requests_made.fetch_add(1, atomic::Ordering::Release);
25959 Ok(Vec::new())
25960 }
25961 });
25962 let save = editor.update_in(cx, |editor, window, cx| {
25963 editor.move_to_end(&MoveToEnd, window, cx);
25964 editor.handle_input("dirty_again", window, cx);
25965 editor.save(
25966 SaveOptions {
25967 format: false,
25968 autosave: true,
25969 },
25970 project.clone(),
25971 window,
25972 cx,
25973 )
25974 });
25975 save.await.unwrap();
25976
25977 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
25978 empty_color_request_handle.next().await.unwrap();
25979 cx.run_until_parked();
25980 assert_eq!(
25981 3,
25982 requests_made.load(atomic::Ordering::Acquire),
25983 "Should query for colors once per save only, as formatting was not requested"
25984 );
25985 editor.update(cx, |editor, cx| {
25986 assert_eq!(
25987 Vec::<Rgba>::new(),
25988 extract_color_inlays(editor, cx),
25989 "Should clear all colors when the server returns an empty response"
25990 );
25991 });
25992}
25993
25994#[gpui::test]
25995async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
25996 init_test(cx, |_| {});
25997 let (editor, cx) = cx.add_window_view(Editor::single_line);
25998 editor.update_in(cx, |editor, window, cx| {
25999 editor.set_text("oops\n\nwow\n", window, cx)
26000 });
26001 cx.run_until_parked();
26002 editor.update(cx, |editor, cx| {
26003 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26004 });
26005 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
26006 cx.run_until_parked();
26007 editor.update(cx, |editor, cx| {
26008 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26009 });
26010}
26011
26012#[gpui::test]
26013async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26014 init_test(cx, |_| {});
26015
26016 cx.update(|cx| {
26017 register_project_item::<Editor>(cx);
26018 });
26019
26020 let fs = FakeFs::new(cx.executor());
26021 fs.insert_tree("/root1", json!({})).await;
26022 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26023 .await;
26024
26025 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26026 let (workspace, cx) =
26027 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26028
26029 let worktree_id = project.update(cx, |project, cx| {
26030 project.worktrees(cx).next().unwrap().read(cx).id()
26031 });
26032
26033 let handle = workspace
26034 .update_in(cx, |workspace, window, cx| {
26035 let project_path = (worktree_id, rel_path("one.pdf"));
26036 workspace.open_path(project_path, None, true, window, cx)
26037 })
26038 .await
26039 .unwrap();
26040
26041 assert_eq!(
26042 handle.to_any().entity_type(),
26043 TypeId::of::<InvalidBufferView>()
26044 );
26045}
26046
26047#[gpui::test]
26048async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26049 init_test(cx, |_| {});
26050
26051 let language = Arc::new(Language::new(
26052 LanguageConfig::default(),
26053 Some(tree_sitter_rust::LANGUAGE.into()),
26054 ));
26055
26056 // Test hierarchical sibling navigation
26057 let text = r#"
26058 fn outer() {
26059 if condition {
26060 let a = 1;
26061 }
26062 let b = 2;
26063 }
26064
26065 fn another() {
26066 let c = 3;
26067 }
26068 "#;
26069
26070 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
26071 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
26072 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
26073
26074 // Wait for parsing to complete
26075 editor
26076 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
26077 .await;
26078
26079 editor.update_in(cx, |editor, window, cx| {
26080 // Start by selecting "let a = 1;" inside the if block
26081 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26082 s.select_display_ranges([
26083 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
26084 ]);
26085 });
26086
26087 let initial_selection = editor.selections.display_ranges(cx);
26088 assert_eq!(initial_selection.len(), 1, "Should have one selection");
26089
26090 // Test select next sibling - should move up levels to find the next sibling
26091 // Since "let a = 1;" has no siblings in the if block, it should move up
26092 // to find "let b = 2;" which is a sibling of the if block
26093 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26094 let next_selection = editor.selections.display_ranges(cx);
26095
26096 // Should have a selection and it should be different from the initial
26097 assert_eq!(
26098 next_selection.len(),
26099 1,
26100 "Should have one selection after next"
26101 );
26102 assert_ne!(
26103 next_selection[0], initial_selection[0],
26104 "Next sibling selection should be different"
26105 );
26106
26107 // Test hierarchical navigation by going to the end of the current function
26108 // and trying to navigate to the next function
26109 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26110 s.select_display_ranges([
26111 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26112 ]);
26113 });
26114
26115 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26116 let function_next_selection = editor.selections.display_ranges(cx);
26117
26118 // Should move to the next function
26119 assert_eq!(
26120 function_next_selection.len(),
26121 1,
26122 "Should have one selection after function next"
26123 );
26124
26125 // Test select previous sibling navigation
26126 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26127 let prev_selection = editor.selections.display_ranges(cx);
26128
26129 // Should have a selection and it should be different
26130 assert_eq!(
26131 prev_selection.len(),
26132 1,
26133 "Should have one selection after prev"
26134 );
26135 assert_ne!(
26136 prev_selection[0], function_next_selection[0],
26137 "Previous sibling selection should be different from next"
26138 );
26139 });
26140}
26141
26142#[gpui::test]
26143async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26144 init_test(cx, |_| {});
26145
26146 let mut cx = EditorTestContext::new(cx).await;
26147 cx.set_state(
26148 "let ˇvariable = 42;
26149let another = variable + 1;
26150let result = variable * 2;",
26151 );
26152
26153 // Set up document highlights manually (simulating LSP response)
26154 cx.update_editor(|editor, _window, cx| {
26155 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26156
26157 // Create highlights for "variable" occurrences
26158 let highlight_ranges = [
26159 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26160 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26161 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26162 ];
26163
26164 let anchor_ranges: Vec<_> = highlight_ranges
26165 .iter()
26166 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26167 .collect();
26168
26169 editor.highlight_background::<DocumentHighlightRead>(
26170 &anchor_ranges,
26171 |theme| theme.colors().editor_document_highlight_read_background,
26172 cx,
26173 );
26174 });
26175
26176 // Go to next highlight - should move to second "variable"
26177 cx.update_editor(|editor, window, cx| {
26178 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26179 });
26180 cx.assert_editor_state(
26181 "let variable = 42;
26182let another = ˇvariable + 1;
26183let result = variable * 2;",
26184 );
26185
26186 // Go to next highlight - should move to third "variable"
26187 cx.update_editor(|editor, window, cx| {
26188 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26189 });
26190 cx.assert_editor_state(
26191 "let variable = 42;
26192let another = variable + 1;
26193let result = ˇvariable * 2;",
26194 );
26195
26196 // Go to next highlight - should stay at third "variable" (no wrap-around)
26197 cx.update_editor(|editor, window, cx| {
26198 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26199 });
26200 cx.assert_editor_state(
26201 "let variable = 42;
26202let another = variable + 1;
26203let result = ˇvariable * 2;",
26204 );
26205
26206 // Now test going backwards from third position
26207 cx.update_editor(|editor, window, cx| {
26208 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26209 });
26210 cx.assert_editor_state(
26211 "let variable = 42;
26212let another = ˇvariable + 1;
26213let result = variable * 2;",
26214 );
26215
26216 // Go to previous highlight - should move to first "variable"
26217 cx.update_editor(|editor, window, cx| {
26218 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26219 });
26220 cx.assert_editor_state(
26221 "let ˇvariable = 42;
26222let another = variable + 1;
26223let result = variable * 2;",
26224 );
26225
26226 // Go to previous highlight - should stay on first "variable"
26227 cx.update_editor(|editor, window, cx| {
26228 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26229 });
26230 cx.assert_editor_state(
26231 "let ˇvariable = 42;
26232let another = variable + 1;
26233let result = variable * 2;",
26234 );
26235}
26236
26237#[gpui::test]
26238async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26239 cx: &mut gpui::TestAppContext,
26240) {
26241 init_test(cx, |_| {});
26242
26243 let url = "https://zed.dev";
26244
26245 let markdown_language = Arc::new(Language::new(
26246 LanguageConfig {
26247 name: "Markdown".into(),
26248 ..LanguageConfig::default()
26249 },
26250 None,
26251 ));
26252
26253 let mut cx = EditorTestContext::new(cx).await;
26254 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26255 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26256
26257 cx.update_editor(|editor, window, cx| {
26258 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26259 editor.paste(&Paste, window, cx);
26260 });
26261
26262 cx.assert_editor_state(&format!(
26263 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26264 ));
26265}
26266
26267#[gpui::test]
26268async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26269 cx: &mut gpui::TestAppContext,
26270) {
26271 init_test(cx, |_| {});
26272
26273 let url = "https://zed.dev";
26274
26275 let markdown_language = Arc::new(Language::new(
26276 LanguageConfig {
26277 name: "Markdown".into(),
26278 ..LanguageConfig::default()
26279 },
26280 None,
26281 ));
26282
26283 let mut cx = EditorTestContext::new(cx).await;
26284 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26285 cx.set_state(&format!(
26286 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26287 ));
26288
26289 cx.update_editor(|editor, window, cx| {
26290 editor.copy(&Copy, window, cx);
26291 });
26292
26293 cx.set_state(&format!(
26294 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26295 ));
26296
26297 cx.update_editor(|editor, window, cx| {
26298 editor.paste(&Paste, window, cx);
26299 });
26300
26301 cx.assert_editor_state(&format!(
26302 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26303 ));
26304}
26305
26306#[gpui::test]
26307async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26308 cx: &mut gpui::TestAppContext,
26309) {
26310 init_test(cx, |_| {});
26311
26312 let url = "https://zed.dev";
26313
26314 let markdown_language = Arc::new(Language::new(
26315 LanguageConfig {
26316 name: "Markdown".into(),
26317 ..LanguageConfig::default()
26318 },
26319 None,
26320 ));
26321
26322 let mut cx = EditorTestContext::new(cx).await;
26323 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26324 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26325
26326 cx.update_editor(|editor, window, cx| {
26327 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26328 editor.paste(&Paste, window, cx);
26329 });
26330
26331 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26332}
26333
26334#[gpui::test]
26335async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26336 cx: &mut gpui::TestAppContext,
26337) {
26338 init_test(cx, |_| {});
26339
26340 let text = "Awesome";
26341
26342 let markdown_language = Arc::new(Language::new(
26343 LanguageConfig {
26344 name: "Markdown".into(),
26345 ..LanguageConfig::default()
26346 },
26347 None,
26348 ));
26349
26350 let mut cx = EditorTestContext::new(cx).await;
26351 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26352 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26353
26354 cx.update_editor(|editor, window, cx| {
26355 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26356 editor.paste(&Paste, window, cx);
26357 });
26358
26359 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26360}
26361
26362#[gpui::test]
26363async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26364 cx: &mut gpui::TestAppContext,
26365) {
26366 init_test(cx, |_| {});
26367
26368 let url = "https://zed.dev";
26369
26370 let markdown_language = Arc::new(Language::new(
26371 LanguageConfig {
26372 name: "Rust".into(),
26373 ..LanguageConfig::default()
26374 },
26375 None,
26376 ));
26377
26378 let mut cx = EditorTestContext::new(cx).await;
26379 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26380 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26381
26382 cx.update_editor(|editor, window, cx| {
26383 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26384 editor.paste(&Paste, window, cx);
26385 });
26386
26387 cx.assert_editor_state(&format!(
26388 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26389 ));
26390}
26391
26392#[gpui::test]
26393async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26394 cx: &mut TestAppContext,
26395) {
26396 init_test(cx, |_| {});
26397
26398 let url = "https://zed.dev";
26399
26400 let markdown_language = Arc::new(Language::new(
26401 LanguageConfig {
26402 name: "Markdown".into(),
26403 ..LanguageConfig::default()
26404 },
26405 None,
26406 ));
26407
26408 let (editor, cx) = cx.add_window_view(|window, cx| {
26409 let multi_buffer = MultiBuffer::build_multi(
26410 [
26411 ("this will embed -> link", vec![Point::row_range(0..1)]),
26412 ("this will replace -> link", vec![Point::row_range(0..1)]),
26413 ],
26414 cx,
26415 );
26416 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26417 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26418 s.select_ranges(vec![
26419 Point::new(0, 19)..Point::new(0, 23),
26420 Point::new(1, 21)..Point::new(1, 25),
26421 ])
26422 });
26423 let first_buffer_id = multi_buffer
26424 .read(cx)
26425 .excerpt_buffer_ids()
26426 .into_iter()
26427 .next()
26428 .unwrap();
26429 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26430 first_buffer.update(cx, |buffer, cx| {
26431 buffer.set_language(Some(markdown_language.clone()), cx);
26432 });
26433
26434 editor
26435 });
26436 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26437
26438 cx.update_editor(|editor, window, cx| {
26439 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26440 editor.paste(&Paste, window, cx);
26441 });
26442
26443 cx.assert_editor_state(&format!(
26444 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26445 ));
26446}
26447
26448#[gpui::test]
26449async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26450 init_test(cx, |_| {});
26451
26452 let fs = FakeFs::new(cx.executor());
26453 fs.insert_tree(
26454 path!("/project"),
26455 json!({
26456 "first.rs": "# First Document\nSome content here.",
26457 "second.rs": "Plain text content for second file.",
26458 }),
26459 )
26460 .await;
26461
26462 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26463 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26464 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26465
26466 let language = rust_lang();
26467 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26468 language_registry.add(language.clone());
26469 let mut fake_servers = language_registry.register_fake_lsp(
26470 "Rust",
26471 FakeLspAdapter {
26472 ..FakeLspAdapter::default()
26473 },
26474 );
26475
26476 let buffer1 = project
26477 .update(cx, |project, cx| {
26478 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26479 })
26480 .await
26481 .unwrap();
26482 let buffer2 = project
26483 .update(cx, |project, cx| {
26484 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26485 })
26486 .await
26487 .unwrap();
26488
26489 let multi_buffer = cx.new(|cx| {
26490 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26491 multi_buffer.set_excerpts_for_path(
26492 PathKey::for_buffer(&buffer1, cx),
26493 buffer1.clone(),
26494 [Point::zero()..buffer1.read(cx).max_point()],
26495 3,
26496 cx,
26497 );
26498 multi_buffer.set_excerpts_for_path(
26499 PathKey::for_buffer(&buffer2, cx),
26500 buffer2.clone(),
26501 [Point::zero()..buffer1.read(cx).max_point()],
26502 3,
26503 cx,
26504 );
26505 multi_buffer
26506 });
26507
26508 let (editor, cx) = cx.add_window_view(|window, cx| {
26509 Editor::new(
26510 EditorMode::full(),
26511 multi_buffer,
26512 Some(project.clone()),
26513 window,
26514 cx,
26515 )
26516 });
26517
26518 let fake_language_server = fake_servers.next().await.unwrap();
26519
26520 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26521
26522 let save = editor.update_in(cx, |editor, window, cx| {
26523 assert!(editor.is_dirty(cx));
26524
26525 editor.save(
26526 SaveOptions {
26527 format: true,
26528 autosave: true,
26529 },
26530 project,
26531 window,
26532 cx,
26533 )
26534 });
26535 let (start_edit_tx, start_edit_rx) = oneshot::channel();
26536 let (done_edit_tx, done_edit_rx) = oneshot::channel();
26537 let mut done_edit_rx = Some(done_edit_rx);
26538 let mut start_edit_tx = Some(start_edit_tx);
26539
26540 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
26541 start_edit_tx.take().unwrap().send(()).unwrap();
26542 let done_edit_rx = done_edit_rx.take().unwrap();
26543 async move {
26544 done_edit_rx.await.unwrap();
26545 Ok(None)
26546 }
26547 });
26548
26549 start_edit_rx.await.unwrap();
26550 buffer2
26551 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
26552 .unwrap();
26553
26554 done_edit_tx.send(()).unwrap();
26555
26556 save.await.unwrap();
26557 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
26558}
26559
26560#[track_caller]
26561fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26562 editor
26563 .all_inlays(cx)
26564 .into_iter()
26565 .filter_map(|inlay| inlay.get_color())
26566 .map(Rgba::from)
26567 .collect()
26568}
26569
26570#[gpui::test]
26571fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
26572 init_test(cx, |_| {});
26573
26574 let editor = cx.add_window(|window, cx| {
26575 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
26576 build_editor(buffer, window, cx)
26577 });
26578
26579 editor
26580 .update(cx, |editor, window, cx| {
26581 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26582 s.select_display_ranges([
26583 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
26584 ])
26585 });
26586
26587 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
26588
26589 assert_eq!(
26590 editor.display_text(cx),
26591 "line1\nline2\nline2",
26592 "Duplicating last line upward should create duplicate above, not on same line"
26593 );
26594
26595 assert_eq!(
26596 editor.selections.display_ranges(cx),
26597 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)],
26598 "Selection should remain on the original line"
26599 );
26600 })
26601 .unwrap();
26602}
26603
26604#[gpui::test]
26605async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
26606 init_test(cx, |_| {});
26607
26608 let mut cx = EditorTestContext::new(cx).await;
26609
26610 cx.set_state("line1\nline2ˇ");
26611
26612 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
26613
26614 let clipboard_text = cx
26615 .read_from_clipboard()
26616 .and_then(|item| item.text().as_deref().map(str::to_string));
26617
26618 assert_eq!(
26619 clipboard_text,
26620 Some("line2\n".to_string()),
26621 "Copying a line without trailing newline should include a newline"
26622 );
26623
26624 cx.set_state("line1\nˇ");
26625
26626 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
26627
26628 cx.assert_editor_state("line1\nline2\nˇ");
26629}