1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionDelegate,
6 element::StickyHeader,
7 linked_editing_ranges::LinkedEditingRanges,
8 scroll::scroll_amount::ScrollAmount,
9 test::{
10 assert_text_with_selections, build_editor,
11 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
12 editor_test_context::EditorTestContext,
13 select_ranges,
14 },
15};
16use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
17use collections::HashMap;
18use futures::{StreamExt, channel::oneshot};
19use gpui::{
20 BackgroundExecutor, DismissEvent, TestAppContext, UpdateGlobal, VisualTestContext,
21 WindowBounds, WindowOptions, div,
22};
23use indoc::indoc;
24use language::{
25 BracketPairConfig,
26 Capability::ReadWrite,
27 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
28 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
29 language_settings::{
30 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
31 },
32 tree_sitter_python,
33};
34use language_settings::Formatter;
35use languages::markdown_lang;
36use languages::rust_lang;
37use lsp::{CompletionParams, DEFAULT_LSP_REQUEST_TIMEOUT};
38use multi_buffer::{
39 ExcerptRange, IndentGuide, MultiBuffer, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey,
40};
41use parking_lot::Mutex;
42use pretty_assertions::{assert_eq, assert_ne};
43use project::{
44 FakeFs, Project,
45 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
46 project_settings::LspSettings,
47 trusted_worktrees::{PathTrust, TrustedWorktrees},
48};
49use serde_json::{self, json};
50use settings::{
51 AllLanguageSettingsContent, DelayMs, EditorSettingsContent, GlobalLspSettingsContent,
52 IndentGuideBackgroundColoring, IndentGuideColoring, InlayHintSettingsContent,
53 ProjectSettingsContent, SearchSettingsContent, SettingsStore,
54};
55use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
56use std::{
57 iter,
58 sync::atomic::{self, AtomicUsize},
59};
60use test::build_editor_with_project;
61use text::ToPoint as _;
62use unindent::Unindent;
63use util::{
64 assert_set_eq, path,
65 rel_path::rel_path,
66 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
67 uri,
68};
69use workspace::{
70 CloseActiveItem, CloseAllItems, CloseOtherItems, NavigationEntry, OpenOptions, ViewId,
71 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
72 register_project_item,
73};
74
75fn display_ranges(editor: &Editor, cx: &mut Context<'_, Editor>) -> Vec<Range<DisplayPoint>> {
76 editor
77 .selections
78 .display_ranges(&editor.display_snapshot(cx))
79}
80
81#[gpui::test]
82fn test_edit_events(cx: &mut TestAppContext) {
83 init_test(cx, |_| {});
84
85 let buffer = cx.new(|cx| {
86 let mut buffer = language::Buffer::local("123456", cx);
87 buffer.set_group_interval(Duration::from_secs(1));
88 buffer
89 });
90
91 let events = Rc::new(RefCell::new(Vec::new()));
92 let editor1 = cx.add_window({
93 let events = events.clone();
94 |window, cx| {
95 let entity = cx.entity();
96 cx.subscribe_in(
97 &entity,
98 window,
99 move |_, _, event: &EditorEvent, _, _| match event {
100 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
101 EditorEvent::BufferEdited => {
102 events.borrow_mut().push(("editor1", "buffer edited"))
103 }
104 _ => {}
105 },
106 )
107 .detach();
108 Editor::for_buffer(buffer.clone(), None, window, cx)
109 }
110 });
111
112 let editor2 = cx.add_window({
113 let events = events.clone();
114 |window, cx| {
115 cx.subscribe_in(
116 &cx.entity(),
117 window,
118 move |_, _, event: &EditorEvent, _, _| match event {
119 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
120 EditorEvent::BufferEdited => {
121 events.borrow_mut().push(("editor2", "buffer edited"))
122 }
123 _ => {}
124 },
125 )
126 .detach();
127 Editor::for_buffer(buffer.clone(), None, window, cx)
128 }
129 });
130
131 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
132
133 // Mutating editor 1 will emit an `Edited` event only for that editor.
134 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
135 assert_eq!(
136 mem::take(&mut *events.borrow_mut()),
137 [
138 ("editor1", "edited"),
139 ("editor1", "buffer edited"),
140 ("editor2", "buffer edited"),
141 ]
142 );
143
144 // Mutating editor 2 will emit an `Edited` event only for that editor.
145 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
146 assert_eq!(
147 mem::take(&mut *events.borrow_mut()),
148 [
149 ("editor2", "edited"),
150 ("editor1", "buffer edited"),
151 ("editor2", "buffer edited"),
152 ]
153 );
154
155 // Undoing on editor 1 will emit an `Edited` event only for that editor.
156 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
157 assert_eq!(
158 mem::take(&mut *events.borrow_mut()),
159 [
160 ("editor1", "edited"),
161 ("editor1", "buffer edited"),
162 ("editor2", "buffer edited"),
163 ]
164 );
165
166 // Redoing on editor 1 will emit an `Edited` event only for that editor.
167 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
168 assert_eq!(
169 mem::take(&mut *events.borrow_mut()),
170 [
171 ("editor1", "edited"),
172 ("editor1", "buffer edited"),
173 ("editor2", "buffer edited"),
174 ]
175 );
176
177 // Undoing on editor 2 will emit an `Edited` event only for that editor.
178 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
179 assert_eq!(
180 mem::take(&mut *events.borrow_mut()),
181 [
182 ("editor2", "edited"),
183 ("editor1", "buffer edited"),
184 ("editor2", "buffer edited"),
185 ]
186 );
187
188 // Redoing on editor 2 will emit an `Edited` event only for that editor.
189 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
190 assert_eq!(
191 mem::take(&mut *events.borrow_mut()),
192 [
193 ("editor2", "edited"),
194 ("editor1", "buffer edited"),
195 ("editor2", "buffer edited"),
196 ]
197 );
198
199 // No event is emitted when the mutation is a no-op.
200 _ = editor2.update(cx, |editor, window, cx| {
201 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
202 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
203 });
204
205 editor.backspace(&Backspace, window, cx);
206 });
207 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
208}
209
210#[gpui::test]
211fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
212 init_test(cx, |_| {});
213
214 let mut now = Instant::now();
215 let group_interval = Duration::from_millis(1);
216 let buffer = cx.new(|cx| {
217 let mut buf = language::Buffer::local("123456", cx);
218 buf.set_group_interval(group_interval);
219 buf
220 });
221 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
222 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
223
224 _ = editor.update(cx, |editor, window, cx| {
225 editor.start_transaction_at(now, window, cx);
226 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
227 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(4)])
228 });
229
230 editor.insert("cd", window, cx);
231 editor.end_transaction_at(now, cx);
232 assert_eq!(editor.text(cx), "12cd56");
233 assert_eq!(
234 editor.selections.ranges(&editor.display_snapshot(cx)),
235 vec![MultiBufferOffset(4)..MultiBufferOffset(4)]
236 );
237
238 editor.start_transaction_at(now, window, cx);
239 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
240 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(5)])
241 });
242 editor.insert("e", window, cx);
243 editor.end_transaction_at(now, cx);
244 assert_eq!(editor.text(cx), "12cde6");
245 assert_eq!(
246 editor.selections.ranges(&editor.display_snapshot(cx)),
247 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
248 );
249
250 now += group_interval + Duration::from_millis(1);
251 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
252 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(2)])
253 });
254
255 // Simulate an edit in another editor
256 buffer.update(cx, |buffer, cx| {
257 buffer.start_transaction_at(now, cx);
258 buffer.edit(
259 [(MultiBufferOffset(0)..MultiBufferOffset(1), "a")],
260 None,
261 cx,
262 );
263 buffer.edit(
264 [(MultiBufferOffset(1)..MultiBufferOffset(1), "b")],
265 None,
266 cx,
267 );
268 buffer.end_transaction_at(now, cx);
269 });
270
271 assert_eq!(editor.text(cx), "ab2cde6");
272 assert_eq!(
273 editor.selections.ranges(&editor.display_snapshot(cx)),
274 vec![MultiBufferOffset(3)..MultiBufferOffset(3)]
275 );
276
277 // Last transaction happened past the group interval in a different editor.
278 // Undo it individually and don't restore selections.
279 editor.undo(&Undo, window, cx);
280 assert_eq!(editor.text(cx), "12cde6");
281 assert_eq!(
282 editor.selections.ranges(&editor.display_snapshot(cx)),
283 vec![MultiBufferOffset(2)..MultiBufferOffset(2)]
284 );
285
286 // First two transactions happened within the group interval in this editor.
287 // Undo them together and restore selections.
288 editor.undo(&Undo, window, cx);
289 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
290 assert_eq!(editor.text(cx), "123456");
291 assert_eq!(
292 editor.selections.ranges(&editor.display_snapshot(cx)),
293 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
294 );
295
296 // Redo the first two transactions together.
297 editor.redo(&Redo, window, cx);
298 assert_eq!(editor.text(cx), "12cde6");
299 assert_eq!(
300 editor.selections.ranges(&editor.display_snapshot(cx)),
301 vec![MultiBufferOffset(5)..MultiBufferOffset(5)]
302 );
303
304 // Redo the last transaction on its own.
305 editor.redo(&Redo, window, cx);
306 assert_eq!(editor.text(cx), "ab2cde6");
307 assert_eq!(
308 editor.selections.ranges(&editor.display_snapshot(cx)),
309 vec![MultiBufferOffset(6)..MultiBufferOffset(6)]
310 );
311
312 // Test empty transactions.
313 editor.start_transaction_at(now, window, cx);
314 editor.end_transaction_at(now, cx);
315 editor.undo(&Undo, window, cx);
316 assert_eq!(editor.text(cx), "12cde6");
317 });
318}
319
320#[gpui::test]
321fn test_ime_composition(cx: &mut TestAppContext) {
322 init_test(cx, |_| {});
323
324 let buffer = cx.new(|cx| {
325 let mut buffer = language::Buffer::local("abcde", cx);
326 // Ensure automatic grouping doesn't occur.
327 buffer.set_group_interval(Duration::ZERO);
328 buffer
329 });
330
331 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
332 cx.add_window(|window, cx| {
333 let mut editor = build_editor(buffer.clone(), window, cx);
334
335 // Start a new IME composition.
336 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
337 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
338 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
339 assert_eq!(editor.text(cx), "äbcde");
340 assert_eq!(
341 editor.marked_text_ranges(cx),
342 Some(vec![
343 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
344 ])
345 );
346
347 // Finalize IME composition.
348 editor.replace_text_in_range(None, "ā", window, cx);
349 assert_eq!(editor.text(cx), "ābcde");
350 assert_eq!(editor.marked_text_ranges(cx), None);
351
352 // IME composition edits are grouped and are undone/redone at once.
353 editor.undo(&Default::default(), window, cx);
354 assert_eq!(editor.text(cx), "abcde");
355 assert_eq!(editor.marked_text_ranges(cx), None);
356 editor.redo(&Default::default(), window, cx);
357 assert_eq!(editor.text(cx), "ābcde");
358 assert_eq!(editor.marked_text_ranges(cx), None);
359
360 // Start a new IME composition.
361 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
362 assert_eq!(
363 editor.marked_text_ranges(cx),
364 Some(vec![
365 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(1))
366 ])
367 );
368
369 // Undoing during an IME composition cancels it.
370 editor.undo(&Default::default(), window, cx);
371 assert_eq!(editor.text(cx), "ābcde");
372 assert_eq!(editor.marked_text_ranges(cx), None);
373
374 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
375 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
376 assert_eq!(editor.text(cx), "ābcdè");
377 assert_eq!(
378 editor.marked_text_ranges(cx),
379 Some(vec![
380 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(5))
381 ])
382 );
383
384 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
385 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
386 assert_eq!(editor.text(cx), "ābcdę");
387 assert_eq!(editor.marked_text_ranges(cx), None);
388
389 // Start a new IME composition with multiple cursors.
390 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
391 s.select_ranges([
392 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(1)),
393 MultiBufferOffsetUtf16(OffsetUtf16(3))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
394 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(5)),
395 ])
396 });
397 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
398 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
399 assert_eq!(
400 editor.marked_text_ranges(cx),
401 Some(vec![
402 MultiBufferOffsetUtf16(OffsetUtf16(0))..MultiBufferOffsetUtf16(OffsetUtf16(3)),
403 MultiBufferOffsetUtf16(OffsetUtf16(4))..MultiBufferOffsetUtf16(OffsetUtf16(7)),
404 MultiBufferOffsetUtf16(OffsetUtf16(8))..MultiBufferOffsetUtf16(OffsetUtf16(11))
405 ])
406 );
407
408 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
409 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
410 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
411 assert_eq!(
412 editor.marked_text_ranges(cx),
413 Some(vec![
414 MultiBufferOffsetUtf16(OffsetUtf16(1))..MultiBufferOffsetUtf16(OffsetUtf16(2)),
415 MultiBufferOffsetUtf16(OffsetUtf16(5))..MultiBufferOffsetUtf16(OffsetUtf16(6)),
416 MultiBufferOffsetUtf16(OffsetUtf16(9))..MultiBufferOffsetUtf16(OffsetUtf16(10))
417 ])
418 );
419
420 // Finalize IME composition with multiple cursors.
421 editor.replace_text_in_range(Some(9..10), "2", window, cx);
422 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
423 assert_eq!(editor.marked_text_ranges(cx), None);
424
425 editor
426 });
427}
428
429#[gpui::test]
430fn test_selection_with_mouse(cx: &mut TestAppContext) {
431 init_test(cx, |_| {});
432
433 let editor = cx.add_window(|window, cx| {
434 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
435 build_editor(buffer, window, cx)
436 });
437
438 _ = editor.update(cx, |editor, window, cx| {
439 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
440 });
441 assert_eq!(
442 editor
443 .update(cx, |editor, _, cx| display_ranges(editor, cx))
444 .unwrap(),
445 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
446 );
447
448 _ = editor.update(cx, |editor, window, cx| {
449 editor.update_selection(
450 DisplayPoint::new(DisplayRow(3), 3),
451 0,
452 gpui::Point::<f32>::default(),
453 window,
454 cx,
455 );
456 });
457
458 assert_eq!(
459 editor
460 .update(cx, |editor, _, cx| display_ranges(editor, cx))
461 .unwrap(),
462 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
463 );
464
465 _ = editor.update(cx, |editor, window, cx| {
466 editor.update_selection(
467 DisplayPoint::new(DisplayRow(1), 1),
468 0,
469 gpui::Point::<f32>::default(),
470 window,
471 cx,
472 );
473 });
474
475 assert_eq!(
476 editor
477 .update(cx, |editor, _, cx| display_ranges(editor, cx))
478 .unwrap(),
479 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
480 );
481
482 _ = editor.update(cx, |editor, window, cx| {
483 editor.end_selection(window, cx);
484 editor.update_selection(
485 DisplayPoint::new(DisplayRow(3), 3),
486 0,
487 gpui::Point::<f32>::default(),
488 window,
489 cx,
490 );
491 });
492
493 assert_eq!(
494 editor
495 .update(cx, |editor, _, cx| display_ranges(editor, cx))
496 .unwrap(),
497 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
498 );
499
500 _ = editor.update(cx, |editor, window, cx| {
501 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
502 editor.update_selection(
503 DisplayPoint::new(DisplayRow(0), 0),
504 0,
505 gpui::Point::<f32>::default(),
506 window,
507 cx,
508 );
509 });
510
511 assert_eq!(
512 editor
513 .update(cx, |editor, _, cx| display_ranges(editor, cx))
514 .unwrap(),
515 [
516 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
517 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
518 ]
519 );
520
521 _ = editor.update(cx, |editor, window, cx| {
522 editor.end_selection(window, cx);
523 });
524
525 assert_eq!(
526 editor
527 .update(cx, |editor, _, cx| display_ranges(editor, cx))
528 .unwrap(),
529 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
530 );
531}
532
533#[gpui::test]
534fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
535 init_test(cx, |_| {});
536
537 let editor = cx.add_window(|window, cx| {
538 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
539 build_editor(buffer, window, cx)
540 });
541
542 _ = editor.update(cx, |editor, window, cx| {
543 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
544 });
545
546 _ = editor.update(cx, |editor, window, cx| {
547 editor.end_selection(window, cx);
548 });
549
550 _ = editor.update(cx, |editor, window, cx| {
551 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
552 });
553
554 _ = editor.update(cx, |editor, window, cx| {
555 editor.end_selection(window, cx);
556 });
557
558 assert_eq!(
559 editor
560 .update(cx, |editor, _, cx| display_ranges(editor, cx))
561 .unwrap(),
562 [
563 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
564 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
565 ]
566 );
567
568 _ = editor.update(cx, |editor, window, cx| {
569 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
570 });
571
572 _ = editor.update(cx, |editor, window, cx| {
573 editor.end_selection(window, cx);
574 });
575
576 assert_eq!(
577 editor
578 .update(cx, |editor, _, cx| display_ranges(editor, cx))
579 .unwrap(),
580 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
581 );
582}
583
584#[gpui::test]
585fn test_canceling_pending_selection(cx: &mut TestAppContext) {
586 init_test(cx, |_| {});
587
588 let editor = cx.add_window(|window, cx| {
589 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
590 build_editor(buffer, window, cx)
591 });
592
593 _ = editor.update(cx, |editor, window, cx| {
594 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
595 assert_eq!(
596 display_ranges(editor, cx),
597 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
598 );
599 });
600
601 _ = editor.update(cx, |editor, window, cx| {
602 editor.update_selection(
603 DisplayPoint::new(DisplayRow(3), 3),
604 0,
605 gpui::Point::<f32>::default(),
606 window,
607 cx,
608 );
609 assert_eq!(
610 display_ranges(editor, cx),
611 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
612 );
613 });
614
615 _ = editor.update(cx, |editor, window, cx| {
616 editor.cancel(&Cancel, window, cx);
617 editor.update_selection(
618 DisplayPoint::new(DisplayRow(1), 1),
619 0,
620 gpui::Point::<f32>::default(),
621 window,
622 cx,
623 );
624 assert_eq!(
625 display_ranges(editor, cx),
626 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
627 );
628 });
629}
630
631#[gpui::test]
632fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
633 init_test(cx, |_| {});
634
635 let editor = cx.add_window(|window, cx| {
636 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
637 build_editor(buffer, window, cx)
638 });
639
640 _ = editor.update(cx, |editor, window, cx| {
641 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
642 assert_eq!(
643 display_ranges(editor, cx),
644 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
645 );
646
647 editor.move_down(&Default::default(), window, cx);
648 assert_eq!(
649 display_ranges(editor, cx),
650 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
651 );
652
653 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
654 assert_eq!(
655 display_ranges(editor, cx),
656 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
657 );
658
659 editor.move_up(&Default::default(), window, cx);
660 assert_eq!(
661 display_ranges(editor, cx),
662 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
663 );
664 });
665}
666
667#[gpui::test]
668fn test_extending_selection(cx: &mut TestAppContext) {
669 init_test(cx, |_| {});
670
671 let editor = cx.add_window(|window, cx| {
672 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
673 build_editor(buffer, window, cx)
674 });
675
676 _ = editor.update(cx, |editor, window, cx| {
677 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
678 editor.end_selection(window, cx);
679 assert_eq!(
680 display_ranges(editor, cx),
681 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
682 );
683
684 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
685 editor.end_selection(window, cx);
686 assert_eq!(
687 display_ranges(editor, cx),
688 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
689 );
690
691 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
692 editor.end_selection(window, cx);
693 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
694 assert_eq!(
695 display_ranges(editor, cx),
696 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
697 );
698
699 editor.update_selection(
700 DisplayPoint::new(DisplayRow(0), 1),
701 0,
702 gpui::Point::<f32>::default(),
703 window,
704 cx,
705 );
706 editor.end_selection(window, cx);
707 assert_eq!(
708 display_ranges(editor, cx),
709 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
710 );
711
712 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
713 editor.end_selection(window, cx);
714 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
715 editor.end_selection(window, cx);
716 assert_eq!(
717 display_ranges(editor, cx),
718 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
719 );
720
721 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
722 assert_eq!(
723 display_ranges(editor, cx),
724 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
725 );
726
727 editor.update_selection(
728 DisplayPoint::new(DisplayRow(0), 6),
729 0,
730 gpui::Point::<f32>::default(),
731 window,
732 cx,
733 );
734 assert_eq!(
735 display_ranges(editor, cx),
736 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
737 );
738
739 editor.update_selection(
740 DisplayPoint::new(DisplayRow(0), 1),
741 0,
742 gpui::Point::<f32>::default(),
743 window,
744 cx,
745 );
746 editor.end_selection(window, cx);
747 assert_eq!(
748 display_ranges(editor, cx),
749 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
750 );
751 });
752}
753
754#[gpui::test]
755fn test_clone(cx: &mut TestAppContext) {
756 init_test(cx, |_| {});
757
758 let (text, selection_ranges) = marked_text_ranges(
759 indoc! {"
760 one
761 two
762 threeˇ
763 four
764 fiveˇ
765 "},
766 true,
767 );
768
769 let editor = cx.add_window(|window, cx| {
770 let buffer = MultiBuffer::build_simple(&text, cx);
771 build_editor(buffer, window, cx)
772 });
773
774 _ = editor.update(cx, |editor, window, cx| {
775 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
776 s.select_ranges(
777 selection_ranges
778 .iter()
779 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
780 )
781 });
782 editor.fold_creases(
783 vec![
784 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
785 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
786 ],
787 true,
788 window,
789 cx,
790 );
791 });
792
793 let cloned_editor = editor
794 .update(cx, |editor, _, cx| {
795 cx.open_window(Default::default(), |window, cx| {
796 cx.new(|cx| editor.clone(window, cx))
797 })
798 })
799 .unwrap()
800 .unwrap();
801
802 let snapshot = editor
803 .update(cx, |e, window, cx| e.snapshot(window, cx))
804 .unwrap();
805 let cloned_snapshot = cloned_editor
806 .update(cx, |e, window, cx| e.snapshot(window, cx))
807 .unwrap();
808
809 assert_eq!(
810 cloned_editor
811 .update(cx, |e, _, cx| e.display_text(cx))
812 .unwrap(),
813 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
814 );
815 assert_eq!(
816 cloned_snapshot
817 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
818 .collect::<Vec<_>>(),
819 snapshot
820 .folds_in_range(MultiBufferOffset(0)..MultiBufferOffset(text.len()))
821 .collect::<Vec<_>>(),
822 );
823 assert_set_eq!(
824 cloned_editor
825 .update(cx, |editor, _, cx| editor
826 .selections
827 .ranges::<Point>(&editor.display_snapshot(cx)))
828 .unwrap(),
829 editor
830 .update(cx, |editor, _, cx| editor
831 .selections
832 .ranges(&editor.display_snapshot(cx)))
833 .unwrap()
834 );
835 assert_set_eq!(
836 cloned_editor
837 .update(cx, |e, _window, cx| e
838 .selections
839 .display_ranges(&e.display_snapshot(cx)))
840 .unwrap(),
841 editor
842 .update(cx, |e, _, cx| e
843 .selections
844 .display_ranges(&e.display_snapshot(cx)))
845 .unwrap()
846 );
847}
848
849#[gpui::test]
850async fn test_navigation_history(cx: &mut TestAppContext) {
851 init_test(cx, |_| {});
852
853 use workspace::item::Item;
854
855 let fs = FakeFs::new(cx.executor());
856 let project = Project::test(fs, [], cx).await;
857 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
858 let pane = workspace
859 .update(cx, |workspace, _, _| workspace.active_pane().clone())
860 .unwrap();
861
862 _ = workspace.update(cx, |_v, window, cx| {
863 cx.new(|cx| {
864 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
865 let mut editor = build_editor(buffer, window, cx);
866 let handle = cx.entity();
867 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
868
869 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
870 editor.nav_history.as_mut().unwrap().pop_backward(cx)
871 }
872
873 // Move the cursor a small distance.
874 // Nothing is added to the navigation history.
875 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
876 s.select_display_ranges([
877 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
878 ])
879 });
880 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
881 s.select_display_ranges([
882 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
883 ])
884 });
885 assert!(pop_history(&mut editor, cx).is_none());
886
887 // Move the cursor a large distance.
888 // The history can jump back to the previous position.
889 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
890 s.select_display_ranges([
891 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
892 ])
893 });
894 let nav_entry = pop_history(&mut editor, cx).unwrap();
895 editor.navigate(nav_entry.data.unwrap(), window, cx);
896 assert_eq!(nav_entry.item.id(), cx.entity_id());
897 assert_eq!(
898 editor
899 .selections
900 .display_ranges(&editor.display_snapshot(cx)),
901 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
902 );
903 assert!(pop_history(&mut editor, cx).is_none());
904
905 // Move the cursor a small distance via the mouse.
906 // Nothing is added to the navigation history.
907 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
908 editor.end_selection(window, cx);
909 assert_eq!(
910 editor
911 .selections
912 .display_ranges(&editor.display_snapshot(cx)),
913 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
914 );
915 assert!(pop_history(&mut editor, cx).is_none());
916
917 // Move the cursor a large distance via the mouse.
918 // The history can jump back to the previous position.
919 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
920 editor.end_selection(window, cx);
921 assert_eq!(
922 editor
923 .selections
924 .display_ranges(&editor.display_snapshot(cx)),
925 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
926 );
927 let nav_entry = pop_history(&mut editor, cx).unwrap();
928 editor.navigate(nav_entry.data.unwrap(), window, cx);
929 assert_eq!(nav_entry.item.id(), cx.entity_id());
930 assert_eq!(
931 editor
932 .selections
933 .display_ranges(&editor.display_snapshot(cx)),
934 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
935 );
936 assert!(pop_history(&mut editor, cx).is_none());
937
938 // Set scroll position to check later
939 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
940 let original_scroll_position = editor
941 .scroll_manager
942 .native_anchor(&editor.display_snapshot(cx), cx);
943
944 // Jump to the end of the document and adjust scroll
945 editor.move_to_end(&MoveToEnd, window, cx);
946 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
947 assert_ne!(
948 editor
949 .scroll_manager
950 .native_anchor(&editor.display_snapshot(cx), cx),
951 original_scroll_position
952 );
953
954 let nav_entry = pop_history(&mut editor, cx).unwrap();
955 editor.navigate(nav_entry.data.unwrap(), window, cx);
956 assert_eq!(
957 editor
958 .scroll_manager
959 .native_anchor(&editor.display_snapshot(cx), cx),
960 original_scroll_position
961 );
962
963 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
964 let mut invalid_anchor = editor
965 .scroll_manager
966 .native_anchor(&editor.display_snapshot(cx), cx)
967 .anchor;
968 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
969 let invalid_point = Point::new(9999, 0);
970 editor.navigate(
971 Arc::new(NavigationData {
972 cursor_anchor: invalid_anchor,
973 cursor_position: invalid_point,
974 scroll_anchor: ScrollAnchor {
975 anchor: invalid_anchor,
976 offset: Default::default(),
977 },
978 scroll_top_row: invalid_point.row,
979 }),
980 window,
981 cx,
982 );
983 assert_eq!(
984 editor
985 .selections
986 .display_ranges(&editor.display_snapshot(cx)),
987 &[editor.max_point(cx)..editor.max_point(cx)]
988 );
989 assert_eq!(
990 editor.scroll_position(cx),
991 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
992 );
993
994 editor
995 })
996 });
997}
998
999#[gpui::test]
1000fn test_cancel(cx: &mut TestAppContext) {
1001 init_test(cx, |_| {});
1002
1003 let editor = cx.add_window(|window, cx| {
1004 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
1005 build_editor(buffer, window, cx)
1006 });
1007
1008 _ = editor.update(cx, |editor, window, cx| {
1009 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
1010 editor.update_selection(
1011 DisplayPoint::new(DisplayRow(1), 1),
1012 0,
1013 gpui::Point::<f32>::default(),
1014 window,
1015 cx,
1016 );
1017 editor.end_selection(window, cx);
1018
1019 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
1020 editor.update_selection(
1021 DisplayPoint::new(DisplayRow(0), 3),
1022 0,
1023 gpui::Point::<f32>::default(),
1024 window,
1025 cx,
1026 );
1027 editor.end_selection(window, cx);
1028 assert_eq!(
1029 display_ranges(editor, cx),
1030 [
1031 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
1032 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
1033 ]
1034 );
1035 });
1036
1037 _ = editor.update(cx, |editor, window, cx| {
1038 editor.cancel(&Cancel, window, cx);
1039 assert_eq!(
1040 display_ranges(editor, cx),
1041 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
1042 );
1043 });
1044
1045 _ = editor.update(cx, |editor, window, cx| {
1046 editor.cancel(&Cancel, window, cx);
1047 assert_eq!(
1048 display_ranges(editor, cx),
1049 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
1050 );
1051 });
1052}
1053
1054#[gpui::test]
1055fn test_fold_action(cx: &mut TestAppContext) {
1056 init_test(cx, |_| {});
1057
1058 let editor = cx.add_window(|window, cx| {
1059 let buffer = MultiBuffer::build_simple(
1060 &"
1061 impl Foo {
1062 // Hello!
1063
1064 fn a() {
1065 1
1066 }
1067
1068 fn b() {
1069 2
1070 }
1071
1072 fn c() {
1073 3
1074 }
1075 }
1076 "
1077 .unindent(),
1078 cx,
1079 );
1080 build_editor(buffer, window, cx)
1081 });
1082
1083 _ = editor.update(cx, |editor, window, cx| {
1084 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1085 s.select_display_ranges([
1086 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1087 ]);
1088 });
1089 editor.fold(&Fold, window, cx);
1090 assert_eq!(
1091 editor.display_text(cx),
1092 "
1093 impl Foo {
1094 // Hello!
1095
1096 fn a() {
1097 1
1098 }
1099
1100 fn b() {⋯
1101 }
1102
1103 fn c() {⋯
1104 }
1105 }
1106 "
1107 .unindent(),
1108 );
1109
1110 editor.fold(&Fold, window, cx);
1111 assert_eq!(
1112 editor.display_text(cx),
1113 "
1114 impl Foo {⋯
1115 }
1116 "
1117 .unindent(),
1118 );
1119
1120 editor.unfold_lines(&UnfoldLines, window, cx);
1121 assert_eq!(
1122 editor.display_text(cx),
1123 "
1124 impl Foo {
1125 // Hello!
1126
1127 fn a() {
1128 1
1129 }
1130
1131 fn b() {⋯
1132 }
1133
1134 fn c() {⋯
1135 }
1136 }
1137 "
1138 .unindent(),
1139 );
1140
1141 editor.unfold_lines(&UnfoldLines, window, cx);
1142 assert_eq!(
1143 editor.display_text(cx),
1144 editor.buffer.read(cx).read(cx).text()
1145 );
1146 });
1147}
1148
1149#[gpui::test]
1150fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1151 init_test(cx, |_| {});
1152
1153 let editor = cx.add_window(|window, cx| {
1154 let buffer = MultiBuffer::build_simple(
1155 &"
1156 class Foo:
1157 # Hello!
1158
1159 def a():
1160 print(1)
1161
1162 def b():
1163 print(2)
1164
1165 def c():
1166 print(3)
1167 "
1168 .unindent(),
1169 cx,
1170 );
1171 build_editor(buffer, window, cx)
1172 });
1173
1174 _ = editor.update(cx, |editor, window, cx| {
1175 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1176 s.select_display_ranges([
1177 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1178 ]);
1179 });
1180 editor.fold(&Fold, window, cx);
1181 assert_eq!(
1182 editor.display_text(cx),
1183 "
1184 class Foo:
1185 # Hello!
1186
1187 def a():
1188 print(1)
1189
1190 def b():⋯
1191
1192 def c():⋯
1193 "
1194 .unindent(),
1195 );
1196
1197 editor.fold(&Fold, window, cx);
1198 assert_eq!(
1199 editor.display_text(cx),
1200 "
1201 class Foo:⋯
1202 "
1203 .unindent(),
1204 );
1205
1206 editor.unfold_lines(&UnfoldLines, window, cx);
1207 assert_eq!(
1208 editor.display_text(cx),
1209 "
1210 class Foo:
1211 # Hello!
1212
1213 def a():
1214 print(1)
1215
1216 def b():⋯
1217
1218 def c():⋯
1219 "
1220 .unindent(),
1221 );
1222
1223 editor.unfold_lines(&UnfoldLines, window, cx);
1224 assert_eq!(
1225 editor.display_text(cx),
1226 editor.buffer.read(cx).read(cx).text()
1227 );
1228 });
1229}
1230
1231#[gpui::test]
1232fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1233 init_test(cx, |_| {});
1234
1235 let editor = cx.add_window(|window, cx| {
1236 let buffer = MultiBuffer::build_simple(
1237 &"
1238 class Foo:
1239 # Hello!
1240
1241 def a():
1242 print(1)
1243
1244 def b():
1245 print(2)
1246
1247
1248 def c():
1249 print(3)
1250
1251
1252 "
1253 .unindent(),
1254 cx,
1255 );
1256 build_editor(buffer, window, cx)
1257 });
1258
1259 _ = editor.update(cx, |editor, window, cx| {
1260 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1261 s.select_display_ranges([
1262 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1263 ]);
1264 });
1265 editor.fold(&Fold, window, cx);
1266 assert_eq!(
1267 editor.display_text(cx),
1268 "
1269 class Foo:
1270 # Hello!
1271
1272 def a():
1273 print(1)
1274
1275 def b():⋯
1276
1277
1278 def c():⋯
1279
1280
1281 "
1282 .unindent(),
1283 );
1284
1285 editor.fold(&Fold, window, cx);
1286 assert_eq!(
1287 editor.display_text(cx),
1288 "
1289 class Foo:⋯
1290
1291
1292 "
1293 .unindent(),
1294 );
1295
1296 editor.unfold_lines(&UnfoldLines, window, cx);
1297 assert_eq!(
1298 editor.display_text(cx),
1299 "
1300 class Foo:
1301 # Hello!
1302
1303 def a():
1304 print(1)
1305
1306 def b():⋯
1307
1308
1309 def c():⋯
1310
1311
1312 "
1313 .unindent(),
1314 );
1315
1316 editor.unfold_lines(&UnfoldLines, window, cx);
1317 assert_eq!(
1318 editor.display_text(cx),
1319 editor.buffer.read(cx).read(cx).text()
1320 );
1321 });
1322}
1323
1324#[gpui::test]
1325fn test_fold_at_level(cx: &mut TestAppContext) {
1326 init_test(cx, |_| {});
1327
1328 let editor = cx.add_window(|window, cx| {
1329 let buffer = MultiBuffer::build_simple(
1330 &"
1331 class Foo:
1332 # Hello!
1333
1334 def a():
1335 print(1)
1336
1337 def b():
1338 print(2)
1339
1340
1341 class Bar:
1342 # World!
1343
1344 def a():
1345 print(1)
1346
1347 def b():
1348 print(2)
1349
1350
1351 "
1352 .unindent(),
1353 cx,
1354 );
1355 build_editor(buffer, window, cx)
1356 });
1357
1358 _ = editor.update(cx, |editor, window, cx| {
1359 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1360 assert_eq!(
1361 editor.display_text(cx),
1362 "
1363 class Foo:
1364 # Hello!
1365
1366 def a():⋯
1367
1368 def b():⋯
1369
1370
1371 class Bar:
1372 # World!
1373
1374 def a():⋯
1375
1376 def b():⋯
1377
1378
1379 "
1380 .unindent(),
1381 );
1382
1383 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1384 assert_eq!(
1385 editor.display_text(cx),
1386 "
1387 class Foo:⋯
1388
1389
1390 class Bar:⋯
1391
1392
1393 "
1394 .unindent(),
1395 );
1396
1397 editor.unfold_all(&UnfoldAll, window, cx);
1398 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1399 assert_eq!(
1400 editor.display_text(cx),
1401 "
1402 class Foo:
1403 # Hello!
1404
1405 def a():
1406 print(1)
1407
1408 def b():
1409 print(2)
1410
1411
1412 class Bar:
1413 # World!
1414
1415 def a():
1416 print(1)
1417
1418 def b():
1419 print(2)
1420
1421
1422 "
1423 .unindent(),
1424 );
1425
1426 assert_eq!(
1427 editor.display_text(cx),
1428 editor.buffer.read(cx).read(cx).text()
1429 );
1430 let (_, positions) = marked_text_ranges(
1431 &"
1432 class Foo:
1433 # Hello!
1434
1435 def a():
1436 print(1)
1437
1438 def b():
1439 p«riˇ»nt(2)
1440
1441
1442 class Bar:
1443 # World!
1444
1445 def a():
1446 «ˇprint(1)
1447
1448 def b():
1449 print(2)»
1450
1451
1452 "
1453 .unindent(),
1454 true,
1455 );
1456
1457 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1458 s.select_ranges(
1459 positions
1460 .iter()
1461 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
1462 )
1463 });
1464
1465 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1466 assert_eq!(
1467 editor.display_text(cx),
1468 "
1469 class Foo:
1470 # Hello!
1471
1472 def a():⋯
1473
1474 def b():
1475 print(2)
1476
1477
1478 class Bar:
1479 # World!
1480
1481 def a():
1482 print(1)
1483
1484 def b():
1485 print(2)
1486
1487
1488 "
1489 .unindent(),
1490 );
1491 });
1492}
1493
1494#[gpui::test]
1495fn test_move_cursor(cx: &mut TestAppContext) {
1496 init_test(cx, |_| {});
1497
1498 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1499 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1500
1501 buffer.update(cx, |buffer, cx| {
1502 buffer.edit(
1503 vec![
1504 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1505 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1506 ],
1507 None,
1508 cx,
1509 );
1510 });
1511 _ = editor.update(cx, |editor, window, cx| {
1512 assert_eq!(
1513 display_ranges(editor, cx),
1514 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1515 );
1516
1517 editor.move_down(&MoveDown, window, cx);
1518 assert_eq!(
1519 display_ranges(editor, cx),
1520 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1521 );
1522
1523 editor.move_right(&MoveRight, window, cx);
1524 assert_eq!(
1525 display_ranges(editor, cx),
1526 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1527 );
1528
1529 editor.move_left(&MoveLeft, window, cx);
1530 assert_eq!(
1531 display_ranges(editor, cx),
1532 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1533 );
1534
1535 editor.move_up(&MoveUp, window, cx);
1536 assert_eq!(
1537 display_ranges(editor, cx),
1538 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1539 );
1540
1541 editor.move_to_end(&MoveToEnd, window, cx);
1542 assert_eq!(
1543 display_ranges(editor, cx),
1544 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1545 );
1546
1547 editor.move_to_beginning(&MoveToBeginning, window, cx);
1548 assert_eq!(
1549 display_ranges(editor, cx),
1550 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1551 );
1552
1553 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1554 s.select_display_ranges([
1555 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1556 ]);
1557 });
1558 editor.select_to_beginning(&SelectToBeginning, window, cx);
1559 assert_eq!(
1560 display_ranges(editor, cx),
1561 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1562 );
1563
1564 editor.select_to_end(&SelectToEnd, window, cx);
1565 assert_eq!(
1566 display_ranges(editor, cx),
1567 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1568 );
1569 });
1570}
1571
1572#[gpui::test]
1573fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1574 init_test(cx, |_| {});
1575
1576 let editor = cx.add_window(|window, cx| {
1577 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1578 build_editor(buffer, window, cx)
1579 });
1580
1581 assert_eq!('🟥'.len_utf8(), 4);
1582 assert_eq!('α'.len_utf8(), 2);
1583
1584 _ = editor.update(cx, |editor, window, cx| {
1585 editor.fold_creases(
1586 vec![
1587 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1588 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1589 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1590 ],
1591 true,
1592 window,
1593 cx,
1594 );
1595 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1596
1597 editor.move_right(&MoveRight, window, cx);
1598 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1599 editor.move_right(&MoveRight, window, cx);
1600 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1601 editor.move_right(&MoveRight, window, cx);
1602 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧⋯".len())]);
1603
1604 editor.move_down(&MoveDown, window, cx);
1605 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1606 editor.move_left(&MoveLeft, window, cx);
1607 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯".len())]);
1608 editor.move_left(&MoveLeft, window, cx);
1609 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab".len())]);
1610 editor.move_left(&MoveLeft, window, cx);
1611 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "a".len())]);
1612
1613 editor.move_down(&MoveDown, window, cx);
1614 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "α".len())]);
1615 editor.move_right(&MoveRight, window, cx);
1616 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ".len())]);
1617 editor.move_right(&MoveRight, window, cx);
1618 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯".len())]);
1619 editor.move_right(&MoveRight, window, cx);
1620 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1621
1622 editor.move_up(&MoveUp, window, cx);
1623 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1624 editor.move_down(&MoveDown, window, cx);
1625 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1626 editor.move_up(&MoveUp, window, cx);
1627 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1628
1629 editor.move_up(&MoveUp, window, cx);
1630 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1631 editor.move_left(&MoveLeft, window, cx);
1632 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1633 editor.move_left(&MoveLeft, window, cx);
1634 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1635 });
1636}
1637
1638#[gpui::test]
1639fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1640 init_test(cx, |_| {});
1641
1642 let editor = cx.add_window(|window, cx| {
1643 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1644 build_editor(buffer, window, cx)
1645 });
1646 _ = editor.update(cx, |editor, window, cx| {
1647 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1648 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1649 });
1650
1651 // moving above start of document should move selection to start of document,
1652 // but the next move down should still be at the original goal_x
1653 editor.move_up(&MoveUp, window, cx);
1654 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1655
1656 editor.move_down(&MoveDown, window, cx);
1657 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "abcd".len())]);
1658
1659 editor.move_down(&MoveDown, window, cx);
1660 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1661
1662 editor.move_down(&MoveDown, window, cx);
1663 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1664
1665 editor.move_down(&MoveDown, window, cx);
1666 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1667
1668 // moving past end of document should not change goal_x
1669 editor.move_down(&MoveDown, window, cx);
1670 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1671
1672 editor.move_down(&MoveDown, window, cx);
1673 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1674
1675 editor.move_up(&MoveUp, window, cx);
1676 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1677
1678 editor.move_up(&MoveUp, window, cx);
1679 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1680
1681 editor.move_up(&MoveUp, window, cx);
1682 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1683 });
1684}
1685
1686#[gpui::test]
1687fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1688 init_test(cx, |_| {});
1689 let move_to_beg = MoveToBeginningOfLine {
1690 stop_at_soft_wraps: true,
1691 stop_at_indent: true,
1692 };
1693
1694 let delete_to_beg = DeleteToBeginningOfLine {
1695 stop_at_indent: false,
1696 };
1697
1698 let move_to_end = MoveToEndOfLine {
1699 stop_at_soft_wraps: true,
1700 };
1701
1702 let editor = cx.add_window(|window, cx| {
1703 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1704 build_editor(buffer, window, cx)
1705 });
1706 _ = editor.update(cx, |editor, window, cx| {
1707 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1708 s.select_display_ranges([
1709 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1710 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1711 ]);
1712 });
1713 });
1714
1715 _ = editor.update(cx, |editor, window, cx| {
1716 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1717 assert_eq!(
1718 display_ranges(editor, cx),
1719 &[
1720 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1721 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1722 ]
1723 );
1724 });
1725
1726 _ = editor.update(cx, |editor, window, cx| {
1727 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1728 assert_eq!(
1729 display_ranges(editor, cx),
1730 &[
1731 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1732 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1733 ]
1734 );
1735 });
1736
1737 _ = editor.update(cx, |editor, window, cx| {
1738 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1739 assert_eq!(
1740 display_ranges(editor, cx),
1741 &[
1742 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1743 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1744 ]
1745 );
1746 });
1747
1748 _ = editor.update(cx, |editor, window, cx| {
1749 editor.move_to_end_of_line(&move_to_end, window, cx);
1750 assert_eq!(
1751 display_ranges(editor, cx),
1752 &[
1753 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1754 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1755 ]
1756 );
1757 });
1758
1759 // Moving to the end of line again is a no-op.
1760 _ = editor.update(cx, |editor, window, cx| {
1761 editor.move_to_end_of_line(&move_to_end, window, cx);
1762 assert_eq!(
1763 display_ranges(editor, cx),
1764 &[
1765 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1766 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1767 ]
1768 );
1769 });
1770
1771 _ = editor.update(cx, |editor, window, cx| {
1772 editor.move_left(&MoveLeft, window, cx);
1773 editor.select_to_beginning_of_line(
1774 &SelectToBeginningOfLine {
1775 stop_at_soft_wraps: true,
1776 stop_at_indent: true,
1777 },
1778 window,
1779 cx,
1780 );
1781 assert_eq!(
1782 display_ranges(editor, cx),
1783 &[
1784 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1785 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1786 ]
1787 );
1788 });
1789
1790 _ = editor.update(cx, |editor, window, cx| {
1791 editor.select_to_beginning_of_line(
1792 &SelectToBeginningOfLine {
1793 stop_at_soft_wraps: true,
1794 stop_at_indent: true,
1795 },
1796 window,
1797 cx,
1798 );
1799 assert_eq!(
1800 display_ranges(editor, cx),
1801 &[
1802 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1803 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1804 ]
1805 );
1806 });
1807
1808 _ = editor.update(cx, |editor, window, cx| {
1809 editor.select_to_beginning_of_line(
1810 &SelectToBeginningOfLine {
1811 stop_at_soft_wraps: true,
1812 stop_at_indent: true,
1813 },
1814 window,
1815 cx,
1816 );
1817 assert_eq!(
1818 display_ranges(editor, cx),
1819 &[
1820 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1821 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1822 ]
1823 );
1824 });
1825
1826 _ = editor.update(cx, |editor, window, cx| {
1827 editor.select_to_end_of_line(
1828 &SelectToEndOfLine {
1829 stop_at_soft_wraps: true,
1830 },
1831 window,
1832 cx,
1833 );
1834 assert_eq!(
1835 display_ranges(editor, cx),
1836 &[
1837 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1838 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1839 ]
1840 );
1841 });
1842
1843 _ = editor.update(cx, |editor, window, cx| {
1844 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1845 assert_eq!(editor.display_text(cx), "ab\n de");
1846 assert_eq!(
1847 display_ranges(editor, cx),
1848 &[
1849 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1850 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1851 ]
1852 );
1853 });
1854
1855 _ = editor.update(cx, |editor, window, cx| {
1856 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1857 assert_eq!(editor.display_text(cx), "\n");
1858 assert_eq!(
1859 display_ranges(editor, cx),
1860 &[
1861 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1862 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1863 ]
1864 );
1865 });
1866}
1867
1868#[gpui::test]
1869fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1870 init_test(cx, |_| {});
1871 let move_to_beg = MoveToBeginningOfLine {
1872 stop_at_soft_wraps: false,
1873 stop_at_indent: false,
1874 };
1875
1876 let move_to_end = MoveToEndOfLine {
1877 stop_at_soft_wraps: false,
1878 };
1879
1880 let editor = cx.add_window(|window, cx| {
1881 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1882 build_editor(buffer, window, cx)
1883 });
1884
1885 _ = editor.update(cx, |editor, window, cx| {
1886 editor.set_wrap_width(Some(140.0.into()), cx);
1887
1888 // We expect the following lines after wrapping
1889 // ```
1890 // thequickbrownfox
1891 // jumpedoverthelazydo
1892 // gs
1893 // ```
1894 // The final `gs` was soft-wrapped onto a new line.
1895 assert_eq!(
1896 "thequickbrownfox\njumpedoverthelaz\nydogs",
1897 editor.display_text(cx),
1898 );
1899
1900 // First, let's assert behavior on the first line, that was not soft-wrapped.
1901 // Start the cursor at the `k` on the first line
1902 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1903 s.select_display_ranges([
1904 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1905 ]);
1906 });
1907
1908 // Moving to the beginning of the line should put us at the beginning of the line.
1909 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1910 assert_eq!(
1911 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1912 display_ranges(editor, cx)
1913 );
1914
1915 // Moving to the end of the line should put us at the end of the line.
1916 editor.move_to_end_of_line(&move_to_end, window, cx);
1917 assert_eq!(
1918 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1919 display_ranges(editor, cx)
1920 );
1921
1922 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1923 // Start the cursor at the last line (`y` that was wrapped to a new line)
1924 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1925 s.select_display_ranges([
1926 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1927 ]);
1928 });
1929
1930 // Moving to the beginning of the line should put us at the start of the second line of
1931 // display text, i.e., the `j`.
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 display_ranges(editor, cx)
1936 );
1937
1938 // Moving to the beginning of the line again should be a no-op.
1939 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1940 assert_eq!(
1941 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1942 display_ranges(editor, cx)
1943 );
1944
1945 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1946 // next display line.
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 display_ranges(editor, cx)
1951 );
1952
1953 // Moving to the end of the line again should be a no-op.
1954 editor.move_to_end_of_line(&move_to_end, window, cx);
1955 assert_eq!(
1956 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1957 display_ranges(editor, cx)
1958 );
1959 });
1960}
1961
1962#[gpui::test]
1963fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1964 init_test(cx, |_| {});
1965
1966 let move_to_beg = MoveToBeginningOfLine {
1967 stop_at_soft_wraps: true,
1968 stop_at_indent: true,
1969 };
1970
1971 let select_to_beg = SelectToBeginningOfLine {
1972 stop_at_soft_wraps: true,
1973 stop_at_indent: true,
1974 };
1975
1976 let delete_to_beg = DeleteToBeginningOfLine {
1977 stop_at_indent: true,
1978 };
1979
1980 let move_to_end = MoveToEndOfLine {
1981 stop_at_soft_wraps: false,
1982 };
1983
1984 let editor = cx.add_window(|window, cx| {
1985 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1986 build_editor(buffer, window, cx)
1987 });
1988
1989 _ = editor.update(cx, |editor, window, cx| {
1990 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1991 s.select_display_ranges([
1992 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1993 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1994 ]);
1995 });
1996
1997 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1998 // and the second cursor at the first non-whitespace character in the line.
1999 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2000 assert_eq!(
2001 display_ranges(editor, cx),
2002 &[
2003 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2004 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2005 ]
2006 );
2007
2008 // Moving to the beginning of the line again should be a no-op for the first cursor,
2009 // and should move the second cursor to the beginning of the line.
2010 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2011 assert_eq!(
2012 display_ranges(editor, cx),
2013 &[
2014 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2015 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
2016 ]
2017 );
2018
2019 // Moving to the beginning of the line again should still be a no-op for the first cursor,
2020 // and should move the second cursor back to the first non-whitespace character in the line.
2021 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2022 assert_eq!(
2023 display_ranges(editor, cx),
2024 &[
2025 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
2026 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2027 ]
2028 );
2029
2030 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
2031 // and to the first non-whitespace character in the line for the second cursor.
2032 editor.move_to_end_of_line(&move_to_end, window, cx);
2033 editor.move_left(&MoveLeft, window, cx);
2034 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2035 assert_eq!(
2036 display_ranges(editor, cx),
2037 &[
2038 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2039 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
2040 ]
2041 );
2042
2043 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2044 // and should select to the beginning of the line for the second cursor.
2045 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2046 assert_eq!(
2047 display_ranges(editor, cx),
2048 &[
2049 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2050 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2051 ]
2052 );
2053
2054 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2055 // and should delete to the first non-whitespace character in the line for the second cursor.
2056 editor.move_to_end_of_line(&move_to_end, window, cx);
2057 editor.move_left(&MoveLeft, window, cx);
2058 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2059 assert_eq!(editor.text(cx), "c\n f");
2060 });
2061}
2062
2063#[gpui::test]
2064fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2065 init_test(cx, |_| {});
2066
2067 let move_to_beg = MoveToBeginningOfLine {
2068 stop_at_soft_wraps: true,
2069 stop_at_indent: true,
2070 };
2071
2072 let editor = cx.add_window(|window, cx| {
2073 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2074 build_editor(buffer, window, cx)
2075 });
2076
2077 _ = editor.update(cx, |editor, window, cx| {
2078 // test cursor between line_start and indent_start
2079 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2080 s.select_display_ranges([
2081 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2082 ]);
2083 });
2084
2085 // cursor should move to line_start
2086 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2087 assert_eq!(
2088 display_ranges(editor, cx),
2089 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2090 );
2091
2092 // cursor should move to indent_start
2093 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2094 assert_eq!(
2095 display_ranges(editor, cx),
2096 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2097 );
2098
2099 // cursor should move to back to line_start
2100 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2101 assert_eq!(
2102 display_ranges(editor, cx),
2103 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2104 );
2105 });
2106}
2107
2108#[gpui::test]
2109fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2110 init_test(cx, |_| {});
2111
2112 let editor = cx.add_window(|window, cx| {
2113 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2114 build_editor(buffer, window, cx)
2115 });
2116 _ = editor.update(cx, |editor, window, cx| {
2117 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2118 s.select_display_ranges([
2119 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2120 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2121 ])
2122 });
2123 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2124 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2125
2126 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2127 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2128
2129 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2130 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2131
2132 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2133 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2134
2135 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2136 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2137
2138 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2139 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2140
2141 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2142 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2143
2144 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2145 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2146
2147 editor.move_right(&MoveRight, window, cx);
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_previous_word_start(&SelectToPreviousWordStart, window, cx);
2156 assert_selection_ranges(
2157 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2158 editor,
2159 cx,
2160 );
2161
2162 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2163 assert_selection_ranges(
2164 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2165 editor,
2166 cx,
2167 );
2168 });
2169}
2170
2171#[gpui::test]
2172fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2173 init_test(cx, |_| {});
2174
2175 let editor = cx.add_window(|window, cx| {
2176 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2177 build_editor(buffer, window, cx)
2178 });
2179
2180 _ = editor.update(cx, |editor, window, cx| {
2181 editor.set_wrap_width(Some(140.0.into()), cx);
2182 assert_eq!(
2183 editor.display_text(cx),
2184 "use one::{\n two::three::\n four::five\n};"
2185 );
2186
2187 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2188 s.select_display_ranges([
2189 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2190 ]);
2191 });
2192
2193 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2194 assert_eq!(
2195 display_ranges(editor, cx),
2196 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2197 );
2198
2199 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2200 assert_eq!(
2201 display_ranges(editor, cx),
2202 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2203 );
2204
2205 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2206 assert_eq!(
2207 display_ranges(editor, cx),
2208 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2209 );
2210
2211 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2212 assert_eq!(
2213 display_ranges(editor, cx),
2214 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2215 );
2216
2217 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2218 assert_eq!(
2219 display_ranges(editor, cx),
2220 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2221 );
2222
2223 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2224 assert_eq!(
2225 display_ranges(editor, cx),
2226 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2227 );
2228 });
2229}
2230
2231#[gpui::test]
2232async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2233 init_test(cx, |_| {});
2234 let mut cx = EditorTestContext::new(cx).await;
2235
2236 let line_height = cx.update_editor(|editor, window, cx| {
2237 editor
2238 .style(cx)
2239 .text
2240 .line_height_in_pixels(window.rem_size())
2241 });
2242 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2243
2244 cx.set_state(
2245 &r#"ˇone
2246 two
2247
2248 three
2249 fourˇ
2250 five
2251
2252 six"#
2253 .unindent(),
2254 );
2255
2256 cx.update_editor(|editor, window, cx| {
2257 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2258 });
2259 cx.assert_editor_state(
2260 &r#"one
2261 two
2262 ˇ
2263 three
2264 four
2265 five
2266 ˇ
2267 six"#
2268 .unindent(),
2269 );
2270
2271 cx.update_editor(|editor, window, cx| {
2272 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2273 });
2274 cx.assert_editor_state(
2275 &r#"one
2276 two
2277
2278 three
2279 four
2280 five
2281 ˇ
2282 sixˇ"#
2283 .unindent(),
2284 );
2285
2286 cx.update_editor(|editor, window, cx| {
2287 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2288 });
2289 cx.assert_editor_state(
2290 &r#"one
2291 two
2292
2293 three
2294 four
2295 five
2296
2297 sixˇ"#
2298 .unindent(),
2299 );
2300
2301 cx.update_editor(|editor, window, cx| {
2302 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2303 });
2304 cx.assert_editor_state(
2305 &r#"one
2306 two
2307
2308 three
2309 four
2310 five
2311 ˇ
2312 six"#
2313 .unindent(),
2314 );
2315
2316 cx.update_editor(|editor, window, cx| {
2317 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2318 });
2319 cx.assert_editor_state(
2320 &r#"one
2321 two
2322 ˇ
2323 three
2324 four
2325 five
2326
2327 six"#
2328 .unindent(),
2329 );
2330
2331 cx.update_editor(|editor, window, cx| {
2332 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2333 });
2334 cx.assert_editor_state(
2335 &r#"ˇone
2336 two
2337
2338 three
2339 four
2340 five
2341
2342 six"#
2343 .unindent(),
2344 );
2345}
2346
2347#[gpui::test]
2348async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2349 init_test(cx, |_| {});
2350 let mut cx = EditorTestContext::new(cx).await;
2351 let line_height = cx.update_editor(|editor, window, cx| {
2352 editor
2353 .style(cx)
2354 .text
2355 .line_height_in_pixels(window.rem_size())
2356 });
2357 let window = cx.window;
2358 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2359
2360 cx.set_state(
2361 r#"ˇone
2362 two
2363 three
2364 four
2365 five
2366 six
2367 seven
2368 eight
2369 nine
2370 ten
2371 "#,
2372 );
2373
2374 cx.update_editor(|editor, window, cx| {
2375 assert_eq!(
2376 editor.snapshot(window, cx).scroll_position(),
2377 gpui::Point::new(0., 0.)
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., 3.)
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., 6.)
2388 );
2389 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2390 assert_eq!(
2391 editor.snapshot(window, cx).scroll_position(),
2392 gpui::Point::new(0., 3.)
2393 );
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., 1.)
2399 );
2400 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2401 assert_eq!(
2402 editor.snapshot(window, cx).scroll_position(),
2403 gpui::Point::new(0., 3.)
2404 );
2405 });
2406}
2407
2408#[gpui::test]
2409async fn test_autoscroll(cx: &mut TestAppContext) {
2410 init_test(cx, |_| {});
2411 let mut cx = EditorTestContext::new(cx).await;
2412
2413 let line_height = cx.update_editor(|editor, window, cx| {
2414 editor.set_vertical_scroll_margin(2, cx);
2415 editor
2416 .style(cx)
2417 .text
2418 .line_height_in_pixels(window.rem_size())
2419 });
2420 let window = cx.window;
2421 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2422
2423 cx.set_state(
2424 r#"ˇone
2425 two
2426 three
2427 four
2428 five
2429 six
2430 seven
2431 eight
2432 nine
2433 ten
2434 "#,
2435 );
2436 cx.update_editor(|editor, window, cx| {
2437 assert_eq!(
2438 editor.snapshot(window, cx).scroll_position(),
2439 gpui::Point::new(0., 0.0)
2440 );
2441 });
2442
2443 // Add a cursor below the visible area. Since both cursors cannot fit
2444 // on screen, the editor autoscrolls to reveal the newest cursor, and
2445 // allows the vertical scroll margin below that cursor.
2446 cx.update_editor(|editor, window, cx| {
2447 editor.change_selections(Default::default(), window, cx, |selections| {
2448 selections.select_ranges([
2449 Point::new(0, 0)..Point::new(0, 0),
2450 Point::new(6, 0)..Point::new(6, 0),
2451 ]);
2452 })
2453 });
2454 cx.update_editor(|editor, window, cx| {
2455 assert_eq!(
2456 editor.snapshot(window, cx).scroll_position(),
2457 gpui::Point::new(0., 3.0)
2458 );
2459 });
2460
2461 // Move down. The editor cursor scrolls down to track the newest cursor.
2462 cx.update_editor(|editor, window, cx| {
2463 editor.move_down(&Default::default(), window, cx);
2464 });
2465 cx.update_editor(|editor, window, cx| {
2466 assert_eq!(
2467 editor.snapshot(window, cx).scroll_position(),
2468 gpui::Point::new(0., 4.0)
2469 );
2470 });
2471
2472 // Add a cursor above the visible area. Since both cursors fit on screen,
2473 // the editor scrolls to show both.
2474 cx.update_editor(|editor, window, cx| {
2475 editor.change_selections(Default::default(), window, cx, |selections| {
2476 selections.select_ranges([
2477 Point::new(1, 0)..Point::new(1, 0),
2478 Point::new(6, 0)..Point::new(6, 0),
2479 ]);
2480 })
2481 });
2482 cx.update_editor(|editor, window, cx| {
2483 assert_eq!(
2484 editor.snapshot(window, cx).scroll_position(),
2485 gpui::Point::new(0., 1.0)
2486 );
2487 });
2488}
2489
2490#[gpui::test]
2491async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2492 init_test(cx, |_| {});
2493 let mut cx = EditorTestContext::new(cx).await;
2494
2495 let line_height = cx.update_editor(|editor, window, cx| {
2496 editor
2497 .style(cx)
2498 .text
2499 .line_height_in_pixels(window.rem_size())
2500 });
2501 let window = cx.window;
2502 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2503 cx.set_state(
2504 &r#"
2505 ˇone
2506 two
2507 threeˇ
2508 four
2509 five
2510 six
2511 seven
2512 eight
2513 nine
2514 ten
2515 "#
2516 .unindent(),
2517 );
2518
2519 cx.update_editor(|editor, window, cx| {
2520 editor.move_page_down(&MovePageDown::default(), window, cx)
2521 });
2522 cx.assert_editor_state(
2523 &r#"
2524 one
2525 two
2526 three
2527 ˇfour
2528 five
2529 sixˇ
2530 seven
2531 eight
2532 nine
2533 ten
2534 "#
2535 .unindent(),
2536 );
2537
2538 cx.update_editor(|editor, window, cx| {
2539 editor.move_page_down(&MovePageDown::default(), window, cx)
2540 });
2541 cx.assert_editor_state(
2542 &r#"
2543 one
2544 two
2545 three
2546 four
2547 five
2548 six
2549 ˇseven
2550 eight
2551 nineˇ
2552 ten
2553 "#
2554 .unindent(),
2555 );
2556
2557 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2558 cx.assert_editor_state(
2559 &r#"
2560 one
2561 two
2562 three
2563 ˇfour
2564 five
2565 sixˇ
2566 seven
2567 eight
2568 nine
2569 ten
2570 "#
2571 .unindent(),
2572 );
2573
2574 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2575 cx.assert_editor_state(
2576 &r#"
2577 ˇone
2578 two
2579 threeˇ
2580 four
2581 five
2582 six
2583 seven
2584 eight
2585 nine
2586 ten
2587 "#
2588 .unindent(),
2589 );
2590
2591 // Test select collapsing
2592 cx.update_editor(|editor, window, cx| {
2593 editor.move_page_down(&MovePageDown::default(), window, cx);
2594 editor.move_page_down(&MovePageDown::default(), window, cx);
2595 editor.move_page_down(&MovePageDown::default(), window, cx);
2596 });
2597 cx.assert_editor_state(
2598 &r#"
2599 one
2600 two
2601 three
2602 four
2603 five
2604 six
2605 seven
2606 eight
2607 nine
2608 ˇten
2609 ˇ"#
2610 .unindent(),
2611 );
2612}
2613
2614#[gpui::test]
2615async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2616 init_test(cx, |_| {});
2617 let mut cx = EditorTestContext::new(cx).await;
2618 cx.set_state("one «two threeˇ» four");
2619 cx.update_editor(|editor, window, cx| {
2620 editor.delete_to_beginning_of_line(
2621 &DeleteToBeginningOfLine {
2622 stop_at_indent: false,
2623 },
2624 window,
2625 cx,
2626 );
2627 assert_eq!(editor.text(cx), " four");
2628 });
2629}
2630
2631#[gpui::test]
2632async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2633 init_test(cx, |_| {});
2634
2635 let mut cx = EditorTestContext::new(cx).await;
2636
2637 // For an empty selection, the preceding word fragment is deleted.
2638 // For non-empty selections, only selected characters are deleted.
2639 cx.set_state("onˇe two t«hreˇ»e four");
2640 cx.update_editor(|editor, window, cx| {
2641 editor.delete_to_previous_word_start(
2642 &DeleteToPreviousWordStart {
2643 ignore_newlines: false,
2644 ignore_brackets: false,
2645 },
2646 window,
2647 cx,
2648 );
2649 });
2650 cx.assert_editor_state("ˇe two tˇe four");
2651
2652 cx.set_state("e tˇwo te «fˇ»our");
2653 cx.update_editor(|editor, window, cx| {
2654 editor.delete_to_next_word_end(
2655 &DeleteToNextWordEnd {
2656 ignore_newlines: false,
2657 ignore_brackets: false,
2658 },
2659 window,
2660 cx,
2661 );
2662 });
2663 cx.assert_editor_state("e tˇ te ˇour");
2664}
2665
2666#[gpui::test]
2667async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2668 init_test(cx, |_| {});
2669
2670 let mut cx = EditorTestContext::new(cx).await;
2671
2672 cx.set_state("here is some text ˇwith a space");
2673 cx.update_editor(|editor, window, cx| {
2674 editor.delete_to_previous_word_start(
2675 &DeleteToPreviousWordStart {
2676 ignore_newlines: false,
2677 ignore_brackets: true,
2678 },
2679 window,
2680 cx,
2681 );
2682 });
2683 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2684 cx.assert_editor_state("here is some textˇwith a space");
2685
2686 cx.set_state("here is some text ˇwith a space");
2687 cx.update_editor(|editor, window, cx| {
2688 editor.delete_to_previous_word_start(
2689 &DeleteToPreviousWordStart {
2690 ignore_newlines: false,
2691 ignore_brackets: false,
2692 },
2693 window,
2694 cx,
2695 );
2696 });
2697 cx.assert_editor_state("here is some textˇwith a space");
2698
2699 cx.set_state("here is some textˇ with a space");
2700 cx.update_editor(|editor, window, cx| {
2701 editor.delete_to_next_word_end(
2702 &DeleteToNextWordEnd {
2703 ignore_newlines: false,
2704 ignore_brackets: true,
2705 },
2706 window,
2707 cx,
2708 );
2709 });
2710 // Same happens in the other direction.
2711 cx.assert_editor_state("here is some textˇwith a space");
2712
2713 cx.set_state("here is some textˇ with a space");
2714 cx.update_editor(|editor, window, cx| {
2715 editor.delete_to_next_word_end(
2716 &DeleteToNextWordEnd {
2717 ignore_newlines: false,
2718 ignore_brackets: false,
2719 },
2720 window,
2721 cx,
2722 );
2723 });
2724 cx.assert_editor_state("here is some textˇwith a space");
2725
2726 cx.set_state("here is some textˇ with a space");
2727 cx.update_editor(|editor, window, cx| {
2728 editor.delete_to_next_word_end(
2729 &DeleteToNextWordEnd {
2730 ignore_newlines: true,
2731 ignore_brackets: false,
2732 },
2733 window,
2734 cx,
2735 );
2736 });
2737 cx.assert_editor_state("here is some textˇwith a space");
2738 cx.update_editor(|editor, window, cx| {
2739 editor.delete_to_previous_word_start(
2740 &DeleteToPreviousWordStart {
2741 ignore_newlines: true,
2742 ignore_brackets: false,
2743 },
2744 window,
2745 cx,
2746 );
2747 });
2748 cx.assert_editor_state("here is some ˇwith a space");
2749 cx.update_editor(|editor, window, cx| {
2750 editor.delete_to_previous_word_start(
2751 &DeleteToPreviousWordStart {
2752 ignore_newlines: true,
2753 ignore_brackets: false,
2754 },
2755 window,
2756 cx,
2757 );
2758 });
2759 // Single whitespaces are removed with the word behind them.
2760 cx.assert_editor_state("here is ˇwith a space");
2761 cx.update_editor(|editor, window, cx| {
2762 editor.delete_to_previous_word_start(
2763 &DeleteToPreviousWordStart {
2764 ignore_newlines: true,
2765 ignore_brackets: false,
2766 },
2767 window,
2768 cx,
2769 );
2770 });
2771 cx.assert_editor_state("here ˇwith a space");
2772 cx.update_editor(|editor, window, cx| {
2773 editor.delete_to_previous_word_start(
2774 &DeleteToPreviousWordStart {
2775 ignore_newlines: true,
2776 ignore_brackets: false,
2777 },
2778 window,
2779 cx,
2780 );
2781 });
2782 cx.assert_editor_state("ˇwith a space");
2783 cx.update_editor(|editor, window, cx| {
2784 editor.delete_to_previous_word_start(
2785 &DeleteToPreviousWordStart {
2786 ignore_newlines: true,
2787 ignore_brackets: false,
2788 },
2789 window,
2790 cx,
2791 );
2792 });
2793 cx.assert_editor_state("ˇwith a space");
2794 cx.update_editor(|editor, window, cx| {
2795 editor.delete_to_next_word_end(
2796 &DeleteToNextWordEnd {
2797 ignore_newlines: true,
2798 ignore_brackets: false,
2799 },
2800 window,
2801 cx,
2802 );
2803 });
2804 // Same happens in the other direction.
2805 cx.assert_editor_state("ˇ a space");
2806 cx.update_editor(|editor, window, cx| {
2807 editor.delete_to_next_word_end(
2808 &DeleteToNextWordEnd {
2809 ignore_newlines: true,
2810 ignore_brackets: false,
2811 },
2812 window,
2813 cx,
2814 );
2815 });
2816 cx.assert_editor_state("ˇ space");
2817 cx.update_editor(|editor, window, cx| {
2818 editor.delete_to_next_word_end(
2819 &DeleteToNextWordEnd {
2820 ignore_newlines: true,
2821 ignore_brackets: false,
2822 },
2823 window,
2824 cx,
2825 );
2826 });
2827 cx.assert_editor_state("ˇ");
2828 cx.update_editor(|editor, window, cx| {
2829 editor.delete_to_next_word_end(
2830 &DeleteToNextWordEnd {
2831 ignore_newlines: true,
2832 ignore_brackets: false,
2833 },
2834 window,
2835 cx,
2836 );
2837 });
2838 cx.assert_editor_state("ˇ");
2839 cx.update_editor(|editor, window, cx| {
2840 editor.delete_to_previous_word_start(
2841 &DeleteToPreviousWordStart {
2842 ignore_newlines: true,
2843 ignore_brackets: false,
2844 },
2845 window,
2846 cx,
2847 );
2848 });
2849 cx.assert_editor_state("ˇ");
2850}
2851
2852#[gpui::test]
2853async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2854 init_test(cx, |_| {});
2855
2856 let language = Arc::new(
2857 Language::new(
2858 LanguageConfig {
2859 brackets: BracketPairConfig {
2860 pairs: vec![
2861 BracketPair {
2862 start: "\"".to_string(),
2863 end: "\"".to_string(),
2864 close: true,
2865 surround: true,
2866 newline: false,
2867 },
2868 BracketPair {
2869 start: "(".to_string(),
2870 end: ")".to_string(),
2871 close: true,
2872 surround: true,
2873 newline: true,
2874 },
2875 ],
2876 ..BracketPairConfig::default()
2877 },
2878 ..LanguageConfig::default()
2879 },
2880 Some(tree_sitter_rust::LANGUAGE.into()),
2881 )
2882 .with_brackets_query(
2883 r#"
2884 ("(" @open ")" @close)
2885 ("\"" @open "\"" @close)
2886 "#,
2887 )
2888 .unwrap(),
2889 );
2890
2891 let mut cx = EditorTestContext::new(cx).await;
2892 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2893
2894 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2895 cx.update_editor(|editor, window, cx| {
2896 editor.delete_to_previous_word_start(
2897 &DeleteToPreviousWordStart {
2898 ignore_newlines: true,
2899 ignore_brackets: false,
2900 },
2901 window,
2902 cx,
2903 );
2904 });
2905 // Deletion stops before brackets if asked to not ignore them.
2906 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2907 cx.update_editor(|editor, window, cx| {
2908 editor.delete_to_previous_word_start(
2909 &DeleteToPreviousWordStart {
2910 ignore_newlines: true,
2911 ignore_brackets: false,
2912 },
2913 window,
2914 cx,
2915 );
2916 });
2917 // Deletion has to remove a single bracket and then stop again.
2918 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2919
2920 cx.update_editor(|editor, window, cx| {
2921 editor.delete_to_previous_word_start(
2922 &DeleteToPreviousWordStart {
2923 ignore_newlines: true,
2924 ignore_brackets: false,
2925 },
2926 window,
2927 cx,
2928 );
2929 });
2930 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2931
2932 cx.update_editor(|editor, window, cx| {
2933 editor.delete_to_previous_word_start(
2934 &DeleteToPreviousWordStart {
2935 ignore_newlines: true,
2936 ignore_brackets: false,
2937 },
2938 window,
2939 cx,
2940 );
2941 });
2942 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2943
2944 cx.update_editor(|editor, window, cx| {
2945 editor.delete_to_previous_word_start(
2946 &DeleteToPreviousWordStart {
2947 ignore_newlines: true,
2948 ignore_brackets: false,
2949 },
2950 window,
2951 cx,
2952 );
2953 });
2954 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2955
2956 cx.update_editor(|editor, window, cx| {
2957 editor.delete_to_next_word_end(
2958 &DeleteToNextWordEnd {
2959 ignore_newlines: true,
2960 ignore_brackets: false,
2961 },
2962 window,
2963 cx,
2964 );
2965 });
2966 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2967 cx.assert_editor_state(r#"ˇ");"#);
2968
2969 cx.update_editor(|editor, window, cx| {
2970 editor.delete_to_next_word_end(
2971 &DeleteToNextWordEnd {
2972 ignore_newlines: true,
2973 ignore_brackets: false,
2974 },
2975 window,
2976 cx,
2977 );
2978 });
2979 cx.assert_editor_state(r#"ˇ"#);
2980
2981 cx.update_editor(|editor, window, cx| {
2982 editor.delete_to_next_word_end(
2983 &DeleteToNextWordEnd {
2984 ignore_newlines: true,
2985 ignore_brackets: false,
2986 },
2987 window,
2988 cx,
2989 );
2990 });
2991 cx.assert_editor_state(r#"ˇ"#);
2992
2993 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2994 cx.update_editor(|editor, window, cx| {
2995 editor.delete_to_previous_word_start(
2996 &DeleteToPreviousWordStart {
2997 ignore_newlines: true,
2998 ignore_brackets: true,
2999 },
3000 window,
3001 cx,
3002 );
3003 });
3004 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
3005}
3006
3007#[gpui::test]
3008fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
3009 init_test(cx, |_| {});
3010
3011 let editor = cx.add_window(|window, cx| {
3012 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
3013 build_editor(buffer, window, cx)
3014 });
3015 let del_to_prev_word_start = DeleteToPreviousWordStart {
3016 ignore_newlines: false,
3017 ignore_brackets: false,
3018 };
3019 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
3020 ignore_newlines: true,
3021 ignore_brackets: false,
3022 };
3023
3024 _ = editor.update(cx, |editor, window, cx| {
3025 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3026 s.select_display_ranges([
3027 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
3028 ])
3029 });
3030 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3031 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
3032 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3033 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
3034 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3035 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
3036 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
3037 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
3038 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3039 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3040 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3041 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3042 });
3043}
3044
3045#[gpui::test]
3046fn test_delete_to_previous_subword_start_or_newline(cx: &mut TestAppContext) {
3047 init_test(cx, |_| {});
3048
3049 let editor = cx.add_window(|window, cx| {
3050 let buffer = MultiBuffer::build_simple("fooBar\n\nbazQux", cx);
3051 build_editor(buffer, window, cx)
3052 });
3053 let del_to_prev_sub_word_start = DeleteToPreviousSubwordStart {
3054 ignore_newlines: false,
3055 ignore_brackets: false,
3056 };
3057 let del_to_prev_sub_word_start_ignore_newlines = DeleteToPreviousSubwordStart {
3058 ignore_newlines: true,
3059 ignore_brackets: false,
3060 };
3061
3062 _ = editor.update(cx, |editor, window, cx| {
3063 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3064 s.select_display_ranges([
3065 DisplayPoint::new(DisplayRow(2), 6)..DisplayPoint::new(DisplayRow(2), 6)
3066 ])
3067 });
3068 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3069 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n\nbaz");
3070 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3071 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n\n");
3072 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3073 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n");
3074 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3075 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar");
3076 editor.delete_to_previous_subword_start(&del_to_prev_sub_word_start, window, cx);
3077 assert_eq!(editor.buffer.read(cx).read(cx).text(), "foo");
3078 editor.delete_to_previous_subword_start(
3079 &del_to_prev_sub_word_start_ignore_newlines,
3080 window,
3081 cx,
3082 );
3083 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3084 });
3085}
3086
3087#[gpui::test]
3088fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3089 init_test(cx, |_| {});
3090
3091 let editor = cx.add_window(|window, cx| {
3092 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3093 build_editor(buffer, window, cx)
3094 });
3095 let del_to_next_word_end = DeleteToNextWordEnd {
3096 ignore_newlines: false,
3097 ignore_brackets: false,
3098 };
3099 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3100 ignore_newlines: true,
3101 ignore_brackets: false,
3102 };
3103
3104 _ = editor.update(cx, |editor, window, cx| {
3105 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3106 s.select_display_ranges([
3107 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3108 ])
3109 });
3110 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3111 assert_eq!(
3112 editor.buffer.read(cx).read(cx).text(),
3113 "one\n two\nthree\n four"
3114 );
3115 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3116 assert_eq!(
3117 editor.buffer.read(cx).read(cx).text(),
3118 "\n two\nthree\n four"
3119 );
3120 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3121 assert_eq!(
3122 editor.buffer.read(cx).read(cx).text(),
3123 "two\nthree\n four"
3124 );
3125 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3126 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3127 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3128 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3129 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3130 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3131 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3132 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3133 });
3134}
3135
3136#[gpui::test]
3137fn test_delete_to_next_subword_end_or_newline(cx: &mut TestAppContext) {
3138 init_test(cx, |_| {});
3139
3140 let editor = cx.add_window(|window, cx| {
3141 let buffer = MultiBuffer::build_simple("\nfooBar\n bazQux", cx);
3142 build_editor(buffer, window, cx)
3143 });
3144 let del_to_next_subword_end = DeleteToNextSubwordEnd {
3145 ignore_newlines: false,
3146 ignore_brackets: false,
3147 };
3148 let del_to_next_subword_end_ignore_newlines = DeleteToNextSubwordEnd {
3149 ignore_newlines: true,
3150 ignore_brackets: false,
3151 };
3152
3153 _ = editor.update(cx, |editor, window, cx| {
3154 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3155 s.select_display_ranges([
3156 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3157 ])
3158 });
3159 // Delete "\n" (empty line)
3160 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3161 assert_eq!(editor.buffer.read(cx).read(cx).text(), "fooBar\n bazQux");
3162 // Delete "foo" (subword boundary)
3163 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3164 assert_eq!(editor.buffer.read(cx).read(cx).text(), "Bar\n bazQux");
3165 // Delete "Bar"
3166 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3167 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n bazQux");
3168 // Delete "\n " (newline + leading whitespace)
3169 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3170 assert_eq!(editor.buffer.read(cx).read(cx).text(), "bazQux");
3171 // Delete "baz" (subword boundary)
3172 editor.delete_to_next_subword_end(&del_to_next_subword_end, window, cx);
3173 assert_eq!(editor.buffer.read(cx).read(cx).text(), "Qux");
3174 // With ignore_newlines, delete "Qux"
3175 editor.delete_to_next_subword_end(&del_to_next_subword_end_ignore_newlines, window, cx);
3176 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3177 });
3178}
3179
3180#[gpui::test]
3181fn test_newline(cx: &mut TestAppContext) {
3182 init_test(cx, |_| {});
3183
3184 let editor = cx.add_window(|window, cx| {
3185 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3186 build_editor(buffer, window, cx)
3187 });
3188
3189 _ = editor.update(cx, |editor, window, cx| {
3190 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3191 s.select_display_ranges([
3192 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3193 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3194 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3195 ])
3196 });
3197
3198 editor.newline(&Newline, window, cx);
3199 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3200 });
3201}
3202
3203#[gpui::test]
3204async fn test_newline_yaml(cx: &mut TestAppContext) {
3205 init_test(cx, |_| {});
3206
3207 let mut cx = EditorTestContext::new(cx).await;
3208 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3209 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3210
3211 // Object (between 2 fields)
3212 cx.set_state(indoc! {"
3213 test:ˇ
3214 hello: bye"});
3215 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3216 cx.assert_editor_state(indoc! {"
3217 test:
3218 ˇ
3219 hello: bye"});
3220
3221 // Object (first and single line)
3222 cx.set_state(indoc! {"
3223 test:ˇ"});
3224 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3225 cx.assert_editor_state(indoc! {"
3226 test:
3227 ˇ"});
3228
3229 // Array with objects (after first element)
3230 cx.set_state(indoc! {"
3231 test:
3232 - foo: barˇ"});
3233 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3234 cx.assert_editor_state(indoc! {"
3235 test:
3236 - foo: bar
3237 ˇ"});
3238
3239 // Array with objects and comment
3240 cx.set_state(indoc! {"
3241 test:
3242 - foo: bar
3243 - bar: # testˇ"});
3244 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3245 cx.assert_editor_state(indoc! {"
3246 test:
3247 - foo: bar
3248 - bar: # test
3249 ˇ"});
3250
3251 // Array with objects (after second element)
3252 cx.set_state(indoc! {"
3253 test:
3254 - foo: bar
3255 - bar: fooˇ"});
3256 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3257 cx.assert_editor_state(indoc! {"
3258 test:
3259 - foo: bar
3260 - bar: foo
3261 ˇ"});
3262
3263 // Array with strings (after first element)
3264 cx.set_state(indoc! {"
3265 test:
3266 - fooˇ"});
3267 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3268 cx.assert_editor_state(indoc! {"
3269 test:
3270 - foo
3271 ˇ"});
3272}
3273
3274#[gpui::test]
3275fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3276 init_test(cx, |_| {});
3277
3278 let editor = cx.add_window(|window, cx| {
3279 let buffer = MultiBuffer::build_simple(
3280 "
3281 a
3282 b(
3283 X
3284 )
3285 c(
3286 X
3287 )
3288 "
3289 .unindent()
3290 .as_str(),
3291 cx,
3292 );
3293 let mut editor = build_editor(buffer, window, cx);
3294 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3295 s.select_ranges([
3296 Point::new(2, 4)..Point::new(2, 5),
3297 Point::new(5, 4)..Point::new(5, 5),
3298 ])
3299 });
3300 editor
3301 });
3302
3303 _ = editor.update(cx, |editor, window, cx| {
3304 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3305 editor.buffer.update(cx, |buffer, cx| {
3306 buffer.edit(
3307 [
3308 (Point::new(1, 2)..Point::new(3, 0), ""),
3309 (Point::new(4, 2)..Point::new(6, 0), ""),
3310 ],
3311 None,
3312 cx,
3313 );
3314 assert_eq!(
3315 buffer.read(cx).text(),
3316 "
3317 a
3318 b()
3319 c()
3320 "
3321 .unindent()
3322 );
3323 });
3324 assert_eq!(
3325 editor.selections.ranges(&editor.display_snapshot(cx)),
3326 &[
3327 Point::new(1, 2)..Point::new(1, 2),
3328 Point::new(2, 2)..Point::new(2, 2),
3329 ],
3330 );
3331
3332 editor.newline(&Newline, window, cx);
3333 assert_eq!(
3334 editor.text(cx),
3335 "
3336 a
3337 b(
3338 )
3339 c(
3340 )
3341 "
3342 .unindent()
3343 );
3344
3345 // The selections are moved after the inserted newlines
3346 assert_eq!(
3347 editor.selections.ranges(&editor.display_snapshot(cx)),
3348 &[
3349 Point::new(2, 0)..Point::new(2, 0),
3350 Point::new(4, 0)..Point::new(4, 0),
3351 ],
3352 );
3353 });
3354}
3355
3356#[gpui::test]
3357async fn test_newline_above(cx: &mut TestAppContext) {
3358 init_test(cx, |settings| {
3359 settings.defaults.tab_size = NonZeroU32::new(4)
3360 });
3361
3362 let language = Arc::new(
3363 Language::new(
3364 LanguageConfig::default(),
3365 Some(tree_sitter_rust::LANGUAGE.into()),
3366 )
3367 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3368 .unwrap(),
3369 );
3370
3371 let mut cx = EditorTestContext::new(cx).await;
3372 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3373 cx.set_state(indoc! {"
3374 const a: ˇA = (
3375 (ˇ
3376 «const_functionˇ»(ˇ),
3377 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3378 )ˇ
3379 ˇ);ˇ
3380 "});
3381
3382 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3383 cx.assert_editor_state(indoc! {"
3384 ˇ
3385 const a: A = (
3386 ˇ
3387 (
3388 ˇ
3389 ˇ
3390 const_function(),
3391 ˇ
3392 ˇ
3393 ˇ
3394 ˇ
3395 something_else,
3396 ˇ
3397 )
3398 ˇ
3399 ˇ
3400 );
3401 "});
3402}
3403
3404#[gpui::test]
3405async fn test_newline_below(cx: &mut TestAppContext) {
3406 init_test(cx, |settings| {
3407 settings.defaults.tab_size = NonZeroU32::new(4)
3408 });
3409
3410 let language = Arc::new(
3411 Language::new(
3412 LanguageConfig::default(),
3413 Some(tree_sitter_rust::LANGUAGE.into()),
3414 )
3415 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3416 .unwrap(),
3417 );
3418
3419 let mut cx = EditorTestContext::new(cx).await;
3420 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3421 cx.set_state(indoc! {"
3422 const a: ˇA = (
3423 (ˇ
3424 «const_functionˇ»(ˇ),
3425 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3426 )ˇ
3427 ˇ);ˇ
3428 "});
3429
3430 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3431 cx.assert_editor_state(indoc! {"
3432 const a: A = (
3433 ˇ
3434 (
3435 ˇ
3436 const_function(),
3437 ˇ
3438 ˇ
3439 something_else,
3440 ˇ
3441 ˇ
3442 ˇ
3443 ˇ
3444 )
3445 ˇ
3446 );
3447 ˇ
3448 ˇ
3449 "});
3450}
3451
3452#[gpui::test]
3453async fn test_newline_comments(cx: &mut TestAppContext) {
3454 init_test(cx, |settings| {
3455 settings.defaults.tab_size = NonZeroU32::new(4)
3456 });
3457
3458 let language = Arc::new(Language::new(
3459 LanguageConfig {
3460 line_comments: vec!["// ".into()],
3461 ..LanguageConfig::default()
3462 },
3463 None,
3464 ));
3465 {
3466 let mut cx = EditorTestContext::new(cx).await;
3467 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3468 cx.set_state(indoc! {"
3469 // Fooˇ
3470 "});
3471
3472 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3473 cx.assert_editor_state(indoc! {"
3474 // Foo
3475 // ˇ
3476 "});
3477 // Ensure that we add comment prefix when existing line contains space
3478 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3479 cx.assert_editor_state(
3480 indoc! {"
3481 // Foo
3482 //s
3483 // ˇ
3484 "}
3485 .replace("s", " ") // s is used as space placeholder to prevent format on save
3486 .as_str(),
3487 );
3488 // Ensure that we add comment prefix when existing line does not contain space
3489 cx.set_state(indoc! {"
3490 // Foo
3491 //ˇ
3492 "});
3493 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3494 cx.assert_editor_state(indoc! {"
3495 // Foo
3496 //
3497 // ˇ
3498 "});
3499 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3500 cx.set_state(indoc! {"
3501 ˇ// Foo
3502 "});
3503 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3504 cx.assert_editor_state(indoc! {"
3505
3506 ˇ// Foo
3507 "});
3508 }
3509 // Ensure that comment continuations can be disabled.
3510 update_test_language_settings(cx, |settings| {
3511 settings.defaults.extend_comment_on_newline = Some(false);
3512 });
3513 let mut cx = EditorTestContext::new(cx).await;
3514 cx.set_state(indoc! {"
3515 // Fooˇ
3516 "});
3517 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3518 cx.assert_editor_state(indoc! {"
3519 // Foo
3520 ˇ
3521 "});
3522}
3523
3524#[gpui::test]
3525async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3526 init_test(cx, |settings| {
3527 settings.defaults.tab_size = NonZeroU32::new(4)
3528 });
3529
3530 let language = Arc::new(Language::new(
3531 LanguageConfig {
3532 line_comments: vec!["// ".into(), "/// ".into()],
3533 ..LanguageConfig::default()
3534 },
3535 None,
3536 ));
3537 {
3538 let mut cx = EditorTestContext::new(cx).await;
3539 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3540 cx.set_state(indoc! {"
3541 //ˇ
3542 "});
3543 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3544 cx.assert_editor_state(indoc! {"
3545 //
3546 // ˇ
3547 "});
3548
3549 cx.set_state(indoc! {"
3550 ///ˇ
3551 "});
3552 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3553 cx.assert_editor_state(indoc! {"
3554 ///
3555 /// ˇ
3556 "});
3557 }
3558}
3559
3560#[gpui::test]
3561async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3562 init_test(cx, |settings| {
3563 settings.defaults.tab_size = NonZeroU32::new(4)
3564 });
3565
3566 let language = Arc::new(
3567 Language::new(
3568 LanguageConfig {
3569 documentation_comment: Some(language::BlockCommentConfig {
3570 start: "/**".into(),
3571 end: "*/".into(),
3572 prefix: "* ".into(),
3573 tab_size: 1,
3574 }),
3575
3576 ..LanguageConfig::default()
3577 },
3578 Some(tree_sitter_rust::LANGUAGE.into()),
3579 )
3580 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3581 .unwrap(),
3582 );
3583
3584 {
3585 let mut cx = EditorTestContext::new(cx).await;
3586 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3587 cx.set_state(indoc! {"
3588 /**ˇ
3589 "});
3590
3591 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3592 cx.assert_editor_state(indoc! {"
3593 /**
3594 * ˇ
3595 "});
3596 // Ensure that if cursor is before the comment start,
3597 // we do not actually insert a comment prefix.
3598 cx.set_state(indoc! {"
3599 ˇ/**
3600 "});
3601 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3602 cx.assert_editor_state(indoc! {"
3603
3604 ˇ/**
3605 "});
3606 // Ensure that if cursor is between it doesn't add comment prefix.
3607 cx.set_state(indoc! {"
3608 /*ˇ*
3609 "});
3610 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3611 cx.assert_editor_state(indoc! {"
3612 /*
3613 ˇ*
3614 "});
3615 // Ensure that if suffix exists on same line after cursor it adds new line.
3616 cx.set_state(indoc! {"
3617 /**ˇ*/
3618 "});
3619 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3620 cx.assert_editor_state(indoc! {"
3621 /**
3622 * ˇ
3623 */
3624 "});
3625 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3626 cx.set_state(indoc! {"
3627 /**ˇ */
3628 "});
3629 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3630 cx.assert_editor_state(indoc! {"
3631 /**
3632 * ˇ
3633 */
3634 "});
3635 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3636 cx.set_state(indoc! {"
3637 /** ˇ*/
3638 "});
3639 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3640 cx.assert_editor_state(
3641 indoc! {"
3642 /**s
3643 * ˇ
3644 */
3645 "}
3646 .replace("s", " ") // s is used as space placeholder to prevent format on save
3647 .as_str(),
3648 );
3649 // Ensure that delimiter space is preserved when newline on already
3650 // spaced delimiter.
3651 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3652 cx.assert_editor_state(
3653 indoc! {"
3654 /**s
3655 *s
3656 * ˇ
3657 */
3658 "}
3659 .replace("s", " ") // s is used as space placeholder to prevent format on save
3660 .as_str(),
3661 );
3662 // Ensure that delimiter space is preserved when space is not
3663 // on existing delimiter.
3664 cx.set_state(indoc! {"
3665 /**
3666 *ˇ
3667 */
3668 "});
3669 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3670 cx.assert_editor_state(indoc! {"
3671 /**
3672 *
3673 * ˇ
3674 */
3675 "});
3676 // Ensure that if suffix exists on same line after cursor it
3677 // doesn't add extra new line if prefix is not on same line.
3678 cx.set_state(indoc! {"
3679 /**
3680 ˇ*/
3681 "});
3682 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3683 cx.assert_editor_state(indoc! {"
3684 /**
3685
3686 ˇ*/
3687 "});
3688 // Ensure that it detects suffix after existing prefix.
3689 cx.set_state(indoc! {"
3690 /**ˇ/
3691 "});
3692 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3693 cx.assert_editor_state(indoc! {"
3694 /**
3695 ˇ/
3696 "});
3697 // Ensure that if suffix exists on same line before
3698 // cursor it does not add comment prefix.
3699 cx.set_state(indoc! {"
3700 /** */ˇ
3701 "});
3702 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3703 cx.assert_editor_state(indoc! {"
3704 /** */
3705 ˇ
3706 "});
3707 // Ensure that if suffix exists on same line before
3708 // cursor it does not add comment prefix.
3709 cx.set_state(indoc! {"
3710 /**
3711 *
3712 */ˇ
3713 "});
3714 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3715 cx.assert_editor_state(indoc! {"
3716 /**
3717 *
3718 */
3719 ˇ
3720 "});
3721
3722 // Ensure that inline comment followed by code
3723 // doesn't add comment prefix on newline
3724 cx.set_state(indoc! {"
3725 /** */ textˇ
3726 "});
3727 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3728 cx.assert_editor_state(indoc! {"
3729 /** */ text
3730 ˇ
3731 "});
3732
3733 // Ensure that text after comment end tag
3734 // doesn't add comment prefix on newline
3735 cx.set_state(indoc! {"
3736 /**
3737 *
3738 */ˇtext
3739 "});
3740 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3741 cx.assert_editor_state(indoc! {"
3742 /**
3743 *
3744 */
3745 ˇtext
3746 "});
3747
3748 // Ensure if not comment block it doesn't
3749 // add comment prefix on newline
3750 cx.set_state(indoc! {"
3751 * textˇ
3752 "});
3753 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3754 cx.assert_editor_state(indoc! {"
3755 * text
3756 ˇ
3757 "});
3758 }
3759 // Ensure that comment continuations can be disabled.
3760 update_test_language_settings(cx, |settings| {
3761 settings.defaults.extend_comment_on_newline = Some(false);
3762 });
3763 let mut cx = EditorTestContext::new(cx).await;
3764 cx.set_state(indoc! {"
3765 /**ˇ
3766 "});
3767 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3768 cx.assert_editor_state(indoc! {"
3769 /**
3770 ˇ
3771 "});
3772}
3773
3774#[gpui::test]
3775async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3776 init_test(cx, |settings| {
3777 settings.defaults.tab_size = NonZeroU32::new(4)
3778 });
3779
3780 let lua_language = Arc::new(Language::new(
3781 LanguageConfig {
3782 line_comments: vec!["--".into()],
3783 block_comment: Some(language::BlockCommentConfig {
3784 start: "--[[".into(),
3785 prefix: "".into(),
3786 end: "]]".into(),
3787 tab_size: 0,
3788 }),
3789 ..LanguageConfig::default()
3790 },
3791 None,
3792 ));
3793
3794 let mut cx = EditorTestContext::new(cx).await;
3795 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3796
3797 // Line with line comment should extend
3798 cx.set_state(indoc! {"
3799 --ˇ
3800 "});
3801 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3802 cx.assert_editor_state(indoc! {"
3803 --
3804 --ˇ
3805 "});
3806
3807 // Line with block comment that matches line comment should not extend
3808 cx.set_state(indoc! {"
3809 --[[ˇ
3810 "});
3811 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3812 cx.assert_editor_state(indoc! {"
3813 --[[
3814 ˇ
3815 "});
3816}
3817
3818#[gpui::test]
3819fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3820 init_test(cx, |_| {});
3821
3822 let editor = cx.add_window(|window, cx| {
3823 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3824 let mut editor = build_editor(buffer, window, cx);
3825 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3826 s.select_ranges([
3827 MultiBufferOffset(3)..MultiBufferOffset(4),
3828 MultiBufferOffset(11)..MultiBufferOffset(12),
3829 MultiBufferOffset(19)..MultiBufferOffset(20),
3830 ])
3831 });
3832 editor
3833 });
3834
3835 _ = editor.update(cx, |editor, window, cx| {
3836 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3837 editor.buffer.update(cx, |buffer, cx| {
3838 buffer.edit(
3839 [
3840 (MultiBufferOffset(2)..MultiBufferOffset(5), ""),
3841 (MultiBufferOffset(10)..MultiBufferOffset(13), ""),
3842 (MultiBufferOffset(18)..MultiBufferOffset(21), ""),
3843 ],
3844 None,
3845 cx,
3846 );
3847 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3848 });
3849 assert_eq!(
3850 editor.selections.ranges(&editor.display_snapshot(cx)),
3851 &[
3852 MultiBufferOffset(2)..MultiBufferOffset(2),
3853 MultiBufferOffset(7)..MultiBufferOffset(7),
3854 MultiBufferOffset(12)..MultiBufferOffset(12)
3855 ],
3856 );
3857
3858 editor.insert("Z", window, cx);
3859 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3860
3861 // The selections are moved after the inserted characters
3862 assert_eq!(
3863 editor.selections.ranges(&editor.display_snapshot(cx)),
3864 &[
3865 MultiBufferOffset(3)..MultiBufferOffset(3),
3866 MultiBufferOffset(9)..MultiBufferOffset(9),
3867 MultiBufferOffset(15)..MultiBufferOffset(15)
3868 ],
3869 );
3870 });
3871}
3872
3873#[gpui::test]
3874async fn test_tab(cx: &mut TestAppContext) {
3875 init_test(cx, |settings| {
3876 settings.defaults.tab_size = NonZeroU32::new(3)
3877 });
3878
3879 let mut cx = EditorTestContext::new(cx).await;
3880 cx.set_state(indoc! {"
3881 ˇabˇc
3882 ˇ🏀ˇ🏀ˇefg
3883 dˇ
3884 "});
3885 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3886 cx.assert_editor_state(indoc! {"
3887 ˇab ˇc
3888 ˇ🏀 ˇ🏀 ˇefg
3889 d ˇ
3890 "});
3891
3892 cx.set_state(indoc! {"
3893 a
3894 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3895 "});
3896 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3897 cx.assert_editor_state(indoc! {"
3898 a
3899 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3900 "});
3901}
3902
3903#[gpui::test]
3904async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3905 init_test(cx, |_| {});
3906
3907 let mut cx = EditorTestContext::new(cx).await;
3908 let language = Arc::new(
3909 Language::new(
3910 LanguageConfig::default(),
3911 Some(tree_sitter_rust::LANGUAGE.into()),
3912 )
3913 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3914 .unwrap(),
3915 );
3916 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3917
3918 // test when all cursors are not at suggested indent
3919 // then simply move to their suggested indent location
3920 cx.set_state(indoc! {"
3921 const a: B = (
3922 c(
3923 ˇ
3924 ˇ )
3925 );
3926 "});
3927 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3928 cx.assert_editor_state(indoc! {"
3929 const a: B = (
3930 c(
3931 ˇ
3932 ˇ)
3933 );
3934 "});
3935
3936 // test cursor already at suggested indent not moving when
3937 // other cursors are yet to reach their suggested indents
3938 cx.set_state(indoc! {"
3939 ˇ
3940 const a: B = (
3941 c(
3942 d(
3943 ˇ
3944 )
3945 ˇ
3946 ˇ )
3947 );
3948 "});
3949 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3950 cx.assert_editor_state(indoc! {"
3951 ˇ
3952 const a: B = (
3953 c(
3954 d(
3955 ˇ
3956 )
3957 ˇ
3958 ˇ)
3959 );
3960 "});
3961 // test when all cursors are at suggested indent then tab is inserted
3962 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3963 cx.assert_editor_state(indoc! {"
3964 ˇ
3965 const a: B = (
3966 c(
3967 d(
3968 ˇ
3969 )
3970 ˇ
3971 ˇ)
3972 );
3973 "});
3974
3975 // test when current indent is less than suggested indent,
3976 // we adjust line to match suggested indent and move cursor to it
3977 //
3978 // when no other cursor is at word boundary, all of them should move
3979 cx.set_state(indoc! {"
3980 const a: B = (
3981 c(
3982 d(
3983 ˇ
3984 ˇ )
3985 ˇ )
3986 );
3987 "});
3988 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3989 cx.assert_editor_state(indoc! {"
3990 const a: B = (
3991 c(
3992 d(
3993 ˇ
3994 ˇ)
3995 ˇ)
3996 );
3997 "});
3998
3999 // test when current indent is less than suggested indent,
4000 // we adjust line to match suggested indent and move cursor to it
4001 //
4002 // when some other cursor is at word boundary, it should not move
4003 cx.set_state(indoc! {"
4004 const a: B = (
4005 c(
4006 d(
4007 ˇ
4008 ˇ )
4009 ˇ)
4010 );
4011 "});
4012 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4013 cx.assert_editor_state(indoc! {"
4014 const a: B = (
4015 c(
4016 d(
4017 ˇ
4018 ˇ)
4019 ˇ)
4020 );
4021 "});
4022
4023 // test when current indent is more than suggested indent,
4024 // we just move cursor to current indent instead of suggested indent
4025 //
4026 // when no other cursor is at word boundary, all of them should move
4027 cx.set_state(indoc! {"
4028 const a: B = (
4029 c(
4030 d(
4031 ˇ
4032 ˇ )
4033 ˇ )
4034 );
4035 "});
4036 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4037 cx.assert_editor_state(indoc! {"
4038 const a: B = (
4039 c(
4040 d(
4041 ˇ
4042 ˇ)
4043 ˇ)
4044 );
4045 "});
4046 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4047 cx.assert_editor_state(indoc! {"
4048 const a: B = (
4049 c(
4050 d(
4051 ˇ
4052 ˇ)
4053 ˇ)
4054 );
4055 "});
4056
4057 // test when current indent is more than suggested indent,
4058 // we just move cursor to current indent instead of suggested indent
4059 //
4060 // when some other cursor is at word boundary, it doesn't move
4061 cx.set_state(indoc! {"
4062 const a: B = (
4063 c(
4064 d(
4065 ˇ
4066 ˇ )
4067 ˇ)
4068 );
4069 "});
4070 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4071 cx.assert_editor_state(indoc! {"
4072 const a: B = (
4073 c(
4074 d(
4075 ˇ
4076 ˇ)
4077 ˇ)
4078 );
4079 "});
4080
4081 // handle auto-indent when there are multiple cursors on the same line
4082 cx.set_state(indoc! {"
4083 const a: B = (
4084 c(
4085 ˇ ˇ
4086 ˇ )
4087 );
4088 "});
4089 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4090 cx.assert_editor_state(indoc! {"
4091 const a: B = (
4092 c(
4093 ˇ
4094 ˇ)
4095 );
4096 "});
4097}
4098
4099#[gpui::test]
4100async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
4101 init_test(cx, |settings| {
4102 settings.defaults.tab_size = NonZeroU32::new(3)
4103 });
4104
4105 let mut cx = EditorTestContext::new(cx).await;
4106 cx.set_state(indoc! {"
4107 ˇ
4108 \t ˇ
4109 \t ˇ
4110 \t ˇ
4111 \t \t\t \t \t\t \t\t \t \t ˇ
4112 "});
4113
4114 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4115 cx.assert_editor_state(indoc! {"
4116 ˇ
4117 \t ˇ
4118 \t ˇ
4119 \t ˇ
4120 \t \t\t \t \t\t \t\t \t \t ˇ
4121 "});
4122}
4123
4124#[gpui::test]
4125async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
4126 init_test(cx, |settings| {
4127 settings.defaults.tab_size = NonZeroU32::new(4)
4128 });
4129
4130 let language = Arc::new(
4131 Language::new(
4132 LanguageConfig::default(),
4133 Some(tree_sitter_rust::LANGUAGE.into()),
4134 )
4135 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
4136 .unwrap(),
4137 );
4138
4139 let mut cx = EditorTestContext::new(cx).await;
4140 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4141 cx.set_state(indoc! {"
4142 fn a() {
4143 if b {
4144 \t ˇc
4145 }
4146 }
4147 "});
4148
4149 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4150 cx.assert_editor_state(indoc! {"
4151 fn a() {
4152 if b {
4153 ˇc
4154 }
4155 }
4156 "});
4157}
4158
4159#[gpui::test]
4160async fn test_indent_outdent(cx: &mut TestAppContext) {
4161 init_test(cx, |settings| {
4162 settings.defaults.tab_size = NonZeroU32::new(4);
4163 });
4164
4165 let mut cx = EditorTestContext::new(cx).await;
4166
4167 cx.set_state(indoc! {"
4168 «oneˇ» «twoˇ»
4169 three
4170 four
4171 "});
4172 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4173 cx.assert_editor_state(indoc! {"
4174 «oneˇ» «twoˇ»
4175 three
4176 four
4177 "});
4178
4179 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4180 cx.assert_editor_state(indoc! {"
4181 «oneˇ» «twoˇ»
4182 three
4183 four
4184 "});
4185
4186 // select across line ending
4187 cx.set_state(indoc! {"
4188 one two
4189 t«hree
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«hree
4196 ˇ» four
4197 "});
4198
4199 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4200 cx.assert_editor_state(indoc! {"
4201 one two
4202 t«hree
4203 ˇ» four
4204 "});
4205
4206 // Ensure that indenting/outdenting works when the cursor is at column 0.
4207 cx.set_state(indoc! {"
4208 one two
4209 ˇthree
4210 four
4211 "});
4212 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4213 cx.assert_editor_state(indoc! {"
4214 one two
4215 ˇthree
4216 four
4217 "});
4218
4219 cx.set_state(indoc! {"
4220 one two
4221 ˇ three
4222 four
4223 "});
4224 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4225 cx.assert_editor_state(indoc! {"
4226 one two
4227 ˇthree
4228 four
4229 "});
4230}
4231
4232#[gpui::test]
4233async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4234 // This is a regression test for issue #33761
4235 init_test(cx, |_| {});
4236
4237 let mut cx = EditorTestContext::new(cx).await;
4238 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4239 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4240
4241 cx.set_state(
4242 r#"ˇ# ingress:
4243ˇ# api:
4244ˇ# enabled: false
4245ˇ# pathType: Prefix
4246ˇ# console:
4247ˇ# enabled: false
4248ˇ# pathType: Prefix
4249"#,
4250 );
4251
4252 // Press tab to indent all lines
4253 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4254
4255 cx.assert_editor_state(
4256 r#" ˇ# ingress:
4257 ˇ# api:
4258 ˇ# enabled: false
4259 ˇ# pathType: Prefix
4260 ˇ# console:
4261 ˇ# enabled: false
4262 ˇ# pathType: Prefix
4263"#,
4264 );
4265}
4266
4267#[gpui::test]
4268async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4269 // This is a test to make sure our fix for issue #33761 didn't break anything
4270 init_test(cx, |_| {});
4271
4272 let mut cx = EditorTestContext::new(cx).await;
4273 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4274 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4275
4276 cx.set_state(
4277 r#"ˇingress:
4278ˇ api:
4279ˇ enabled: false
4280ˇ pathType: Prefix
4281"#,
4282 );
4283
4284 // Press tab to indent all lines
4285 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4286
4287 cx.assert_editor_state(
4288 r#"ˇingress:
4289 ˇapi:
4290 ˇenabled: false
4291 ˇpathType: Prefix
4292"#,
4293 );
4294}
4295
4296#[gpui::test]
4297async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4298 init_test(cx, |settings| {
4299 settings.defaults.hard_tabs = Some(true);
4300 });
4301
4302 let mut cx = EditorTestContext::new(cx).await;
4303
4304 // select two ranges on one line
4305 cx.set_state(indoc! {"
4306 «oneˇ» «twoˇ»
4307 three
4308 four
4309 "});
4310 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4311 cx.assert_editor_state(indoc! {"
4312 \t«oneˇ» «twoˇ»
4313 three
4314 four
4315 "});
4316 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4317 cx.assert_editor_state(indoc! {"
4318 \t\t«oneˇ» «twoˇ»
4319 three
4320 four
4321 "});
4322 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4323 cx.assert_editor_state(indoc! {"
4324 \t«oneˇ» «twoˇ»
4325 three
4326 four
4327 "});
4328 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4329 cx.assert_editor_state(indoc! {"
4330 «oneˇ» «twoˇ»
4331 three
4332 four
4333 "});
4334
4335 // select across a line ending
4336 cx.set_state(indoc! {"
4337 one two
4338 t«hree
4339 ˇ»four
4340 "});
4341 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4342 cx.assert_editor_state(indoc! {"
4343 one two
4344 \tt«hree
4345 ˇ»four
4346 "});
4347 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4348 cx.assert_editor_state(indoc! {"
4349 one two
4350 \t\tt«hree
4351 ˇ»four
4352 "});
4353 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4354 cx.assert_editor_state(indoc! {"
4355 one two
4356 \tt«hree
4357 ˇ»four
4358 "});
4359 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4360 cx.assert_editor_state(indoc! {"
4361 one two
4362 t«hree
4363 ˇ»four
4364 "});
4365
4366 // Ensure that indenting/outdenting works when the cursor is at column 0.
4367 cx.set_state(indoc! {"
4368 one two
4369 ˇthree
4370 four
4371 "});
4372 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4373 cx.assert_editor_state(indoc! {"
4374 one two
4375 ˇthree
4376 four
4377 "});
4378 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4379 cx.assert_editor_state(indoc! {"
4380 one two
4381 \tˇthree
4382 four
4383 "});
4384 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4385 cx.assert_editor_state(indoc! {"
4386 one two
4387 ˇthree
4388 four
4389 "});
4390}
4391
4392#[gpui::test]
4393fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4394 init_test(cx, |settings| {
4395 settings.languages.0.extend([
4396 (
4397 "TOML".into(),
4398 LanguageSettingsContent {
4399 tab_size: NonZeroU32::new(2),
4400 ..Default::default()
4401 },
4402 ),
4403 (
4404 "Rust".into(),
4405 LanguageSettingsContent {
4406 tab_size: NonZeroU32::new(4),
4407 ..Default::default()
4408 },
4409 ),
4410 ]);
4411 });
4412
4413 let toml_language = Arc::new(Language::new(
4414 LanguageConfig {
4415 name: "TOML".into(),
4416 ..Default::default()
4417 },
4418 None,
4419 ));
4420 let rust_language = Arc::new(Language::new(
4421 LanguageConfig {
4422 name: "Rust".into(),
4423 ..Default::default()
4424 },
4425 None,
4426 ));
4427
4428 let toml_buffer =
4429 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4430 let rust_buffer =
4431 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4432 let multibuffer = cx.new(|cx| {
4433 let mut multibuffer = MultiBuffer::new(ReadWrite);
4434 multibuffer.push_excerpts(
4435 toml_buffer.clone(),
4436 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4437 cx,
4438 );
4439 multibuffer.push_excerpts(
4440 rust_buffer.clone(),
4441 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4442 cx,
4443 );
4444 multibuffer
4445 });
4446
4447 cx.add_window(|window, cx| {
4448 let mut editor = build_editor(multibuffer, window, cx);
4449
4450 assert_eq!(
4451 editor.text(cx),
4452 indoc! {"
4453 a = 1
4454 b = 2
4455
4456 const c: usize = 3;
4457 "}
4458 );
4459
4460 select_ranges(
4461 &mut editor,
4462 indoc! {"
4463 «aˇ» = 1
4464 b = 2
4465
4466 «const c:ˇ» usize = 3;
4467 "},
4468 window,
4469 cx,
4470 );
4471
4472 editor.tab(&Tab, window, cx);
4473 assert_text_with_selections(
4474 &mut editor,
4475 indoc! {"
4476 «aˇ» = 1
4477 b = 2
4478
4479 «const c:ˇ» usize = 3;
4480 "},
4481 cx,
4482 );
4483 editor.backtab(&Backtab, window, cx);
4484 assert_text_with_selections(
4485 &mut editor,
4486 indoc! {"
4487 «aˇ» = 1
4488 b = 2
4489
4490 «const c:ˇ» usize = 3;
4491 "},
4492 cx,
4493 );
4494
4495 editor
4496 });
4497}
4498
4499#[gpui::test]
4500async fn test_backspace(cx: &mut TestAppContext) {
4501 init_test(cx, |_| {});
4502
4503 let mut cx = EditorTestContext::new(cx).await;
4504
4505 // Basic backspace
4506 cx.set_state(indoc! {"
4507 onˇe two three
4508 fou«rˇ» five six
4509 seven «ˇeight nine
4510 »ten
4511 "});
4512 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4513 cx.assert_editor_state(indoc! {"
4514 oˇe two three
4515 fouˇ five six
4516 seven ˇten
4517 "});
4518
4519 // Test backspace inside and around indents
4520 cx.set_state(indoc! {"
4521 zero
4522 ˇone
4523 ˇtwo
4524 ˇ ˇ ˇ three
4525 ˇ ˇ four
4526 "});
4527 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4528 cx.assert_editor_state(indoc! {"
4529 zero
4530 ˇone
4531 ˇtwo
4532 ˇ threeˇ four
4533 "});
4534}
4535
4536#[gpui::test]
4537async fn test_delete(cx: &mut TestAppContext) {
4538 init_test(cx, |_| {});
4539
4540 let mut cx = EditorTestContext::new(cx).await;
4541 cx.set_state(indoc! {"
4542 onˇe two three
4543 fou«rˇ» five six
4544 seven «ˇeight nine
4545 »ten
4546 "});
4547 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4548 cx.assert_editor_state(indoc! {"
4549 onˇ two three
4550 fouˇ five six
4551 seven ˇten
4552 "});
4553}
4554
4555#[gpui::test]
4556fn test_delete_line(cx: &mut TestAppContext) {
4557 init_test(cx, |_| {});
4558
4559 let editor = cx.add_window(|window, cx| {
4560 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4561 build_editor(buffer, window, cx)
4562 });
4563 _ = editor.update(cx, |editor, window, cx| {
4564 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4565 s.select_display_ranges([
4566 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4567 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4568 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4569 ])
4570 });
4571 editor.delete_line(&DeleteLine, window, cx);
4572 assert_eq!(editor.display_text(cx), "ghi");
4573 assert_eq!(
4574 display_ranges(editor, cx),
4575 vec![
4576 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4577 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4578 ]
4579 );
4580 });
4581
4582 let editor = cx.add_window(|window, cx| {
4583 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4584 build_editor(buffer, window, cx)
4585 });
4586 _ = editor.update(cx, |editor, window, cx| {
4587 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4588 s.select_display_ranges([
4589 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4590 ])
4591 });
4592 editor.delete_line(&DeleteLine, window, cx);
4593 assert_eq!(editor.display_text(cx), "ghi\n");
4594 assert_eq!(
4595 display_ranges(editor, cx),
4596 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4597 );
4598 });
4599
4600 let editor = cx.add_window(|window, cx| {
4601 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4602 build_editor(buffer, window, cx)
4603 });
4604 _ = editor.update(cx, |editor, window, cx| {
4605 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4606 s.select_display_ranges([
4607 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4608 ])
4609 });
4610 editor.delete_line(&DeleteLine, window, cx);
4611 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4612 assert_eq!(
4613 display_ranges(editor, cx),
4614 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4615 );
4616 });
4617}
4618
4619#[gpui::test]
4620fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4621 init_test(cx, |_| {});
4622
4623 cx.add_window(|window, cx| {
4624 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4625 let mut editor = build_editor(buffer.clone(), window, cx);
4626 let buffer = buffer.read(cx).as_singleton().unwrap();
4627
4628 assert_eq!(
4629 editor
4630 .selections
4631 .ranges::<Point>(&editor.display_snapshot(cx)),
4632 &[Point::new(0, 0)..Point::new(0, 0)]
4633 );
4634
4635 // When on single line, replace newline at end by space
4636 editor.join_lines(&JoinLines, window, cx);
4637 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4638 assert_eq!(
4639 editor
4640 .selections
4641 .ranges::<Point>(&editor.display_snapshot(cx)),
4642 &[Point::new(0, 3)..Point::new(0, 3)]
4643 );
4644
4645 // When multiple lines are selected, remove newlines that are spanned by the selection
4646 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4647 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4648 });
4649 editor.join_lines(&JoinLines, window, cx);
4650 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4651 assert_eq!(
4652 editor
4653 .selections
4654 .ranges::<Point>(&editor.display_snapshot(cx)),
4655 &[Point::new(0, 11)..Point::new(0, 11)]
4656 );
4657
4658 // Undo should be transactional
4659 editor.undo(&Undo, window, cx);
4660 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4661 assert_eq!(
4662 editor
4663 .selections
4664 .ranges::<Point>(&editor.display_snapshot(cx)),
4665 &[Point::new(0, 5)..Point::new(2, 2)]
4666 );
4667
4668 // When joining an empty line don't insert a space
4669 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4670 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4671 });
4672 editor.join_lines(&JoinLines, window, cx);
4673 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4674 assert_eq!(
4675 editor
4676 .selections
4677 .ranges::<Point>(&editor.display_snapshot(cx)),
4678 [Point::new(2, 3)..Point::new(2, 3)]
4679 );
4680
4681 // We can remove trailing newlines
4682 editor.join_lines(&JoinLines, window, cx);
4683 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4684 assert_eq!(
4685 editor
4686 .selections
4687 .ranges::<Point>(&editor.display_snapshot(cx)),
4688 [Point::new(2, 3)..Point::new(2, 3)]
4689 );
4690
4691 // We don't blow up on the last line
4692 editor.join_lines(&JoinLines, window, cx);
4693 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4694 assert_eq!(
4695 editor
4696 .selections
4697 .ranges::<Point>(&editor.display_snapshot(cx)),
4698 [Point::new(2, 3)..Point::new(2, 3)]
4699 );
4700
4701 // reset to test indentation
4702 editor.buffer.update(cx, |buffer, cx| {
4703 buffer.edit(
4704 [
4705 (Point::new(1, 0)..Point::new(1, 2), " "),
4706 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4707 ],
4708 None,
4709 cx,
4710 )
4711 });
4712
4713 // We remove any leading spaces
4714 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4715 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4716 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4717 });
4718 editor.join_lines(&JoinLines, window, cx);
4719 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4720
4721 // We don't insert a space for a line containing only spaces
4722 editor.join_lines(&JoinLines, window, cx);
4723 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4724
4725 // We ignore any leading tabs
4726 editor.join_lines(&JoinLines, window, cx);
4727 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4728
4729 editor
4730 });
4731}
4732
4733#[gpui::test]
4734fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4735 init_test(cx, |_| {});
4736
4737 cx.add_window(|window, cx| {
4738 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4739 let mut editor = build_editor(buffer.clone(), window, cx);
4740 let buffer = buffer.read(cx).as_singleton().unwrap();
4741
4742 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4743 s.select_ranges([
4744 Point::new(0, 2)..Point::new(1, 1),
4745 Point::new(1, 2)..Point::new(1, 2),
4746 Point::new(3, 1)..Point::new(3, 2),
4747 ])
4748 });
4749
4750 editor.join_lines(&JoinLines, window, cx);
4751 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4752
4753 assert_eq!(
4754 editor
4755 .selections
4756 .ranges::<Point>(&editor.display_snapshot(cx)),
4757 [
4758 Point::new(0, 7)..Point::new(0, 7),
4759 Point::new(1, 3)..Point::new(1, 3)
4760 ]
4761 );
4762 editor
4763 });
4764}
4765
4766#[gpui::test]
4767async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4768 init_test(cx, |_| {});
4769
4770 let mut cx = EditorTestContext::new(cx).await;
4771
4772 let diff_base = r#"
4773 Line 0
4774 Line 1
4775 Line 2
4776 Line 3
4777 "#
4778 .unindent();
4779
4780 cx.set_state(
4781 &r#"
4782 ˇLine 0
4783 Line 1
4784 Line 2
4785 Line 3
4786 "#
4787 .unindent(),
4788 );
4789
4790 cx.set_head_text(&diff_base);
4791 executor.run_until_parked();
4792
4793 // Join lines
4794 cx.update_editor(|editor, window, cx| {
4795 editor.join_lines(&JoinLines, window, cx);
4796 });
4797 executor.run_until_parked();
4798
4799 cx.assert_editor_state(
4800 &r#"
4801 Line 0ˇ Line 1
4802 Line 2
4803 Line 3
4804 "#
4805 .unindent(),
4806 );
4807 // Join again
4808 cx.update_editor(|editor, window, cx| {
4809 editor.join_lines(&JoinLines, window, cx);
4810 });
4811 executor.run_until_parked();
4812
4813 cx.assert_editor_state(
4814 &r#"
4815 Line 0 Line 1ˇ Line 2
4816 Line 3
4817 "#
4818 .unindent(),
4819 );
4820}
4821
4822#[gpui::test]
4823async fn test_custom_newlines_cause_no_false_positive_diffs(
4824 executor: BackgroundExecutor,
4825 cx: &mut TestAppContext,
4826) {
4827 init_test(cx, |_| {});
4828 let mut cx = EditorTestContext::new(cx).await;
4829 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4830 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4831 executor.run_until_parked();
4832
4833 cx.update_editor(|editor, window, cx| {
4834 let snapshot = editor.snapshot(window, cx);
4835 assert_eq!(
4836 snapshot
4837 .buffer_snapshot()
4838 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
4839 .collect::<Vec<_>>(),
4840 Vec::new(),
4841 "Should not have any diffs for files with custom newlines"
4842 );
4843 });
4844}
4845
4846#[gpui::test]
4847async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4848 init_test(cx, |_| {});
4849
4850 let mut cx = EditorTestContext::new(cx).await;
4851
4852 // Test sort_lines_case_insensitive()
4853 cx.set_state(indoc! {"
4854 «z
4855 y
4856 x
4857 Z
4858 Y
4859 Xˇ»
4860 "});
4861 cx.update_editor(|e, window, cx| {
4862 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4863 });
4864 cx.assert_editor_state(indoc! {"
4865 «x
4866 X
4867 y
4868 Y
4869 z
4870 Zˇ»
4871 "});
4872
4873 // Test sort_lines_by_length()
4874 //
4875 // Demonstrates:
4876 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4877 // - sort is stable
4878 cx.set_state(indoc! {"
4879 «123
4880 æ
4881 12
4882 ∞
4883 1
4884 æˇ»
4885 "});
4886 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4887 cx.assert_editor_state(indoc! {"
4888 «æ
4889 ∞
4890 1
4891 æ
4892 12
4893 123ˇ»
4894 "});
4895
4896 // Test reverse_lines()
4897 cx.set_state(indoc! {"
4898 «5
4899 4
4900 3
4901 2
4902 1ˇ»
4903 "});
4904 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4905 cx.assert_editor_state(indoc! {"
4906 «1
4907 2
4908 3
4909 4
4910 5ˇ»
4911 "});
4912
4913 // Skip testing shuffle_line()
4914
4915 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4916 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4917
4918 // Don't manipulate when cursor is on single line, but expand the selection
4919 cx.set_state(indoc! {"
4920 ddˇdd
4921 ccc
4922 bb
4923 a
4924 "});
4925 cx.update_editor(|e, window, cx| {
4926 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4927 });
4928 cx.assert_editor_state(indoc! {"
4929 «ddddˇ»
4930 ccc
4931 bb
4932 a
4933 "});
4934
4935 // Basic manipulate case
4936 // Start selection moves to column 0
4937 // End of selection shrinks to fit shorter line
4938 cx.set_state(indoc! {"
4939 dd«d
4940 ccc
4941 bb
4942 aaaaaˇ»
4943 "});
4944 cx.update_editor(|e, window, cx| {
4945 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4946 });
4947 cx.assert_editor_state(indoc! {"
4948 «aaaaa
4949 bb
4950 ccc
4951 dddˇ»
4952 "});
4953
4954 // Manipulate case with newlines
4955 cx.set_state(indoc! {"
4956 dd«d
4957 ccc
4958
4959 bb
4960 aaaaa
4961
4962 ˇ»
4963 "});
4964 cx.update_editor(|e, window, cx| {
4965 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4966 });
4967 cx.assert_editor_state(indoc! {"
4968 «
4969
4970 aaaaa
4971 bb
4972 ccc
4973 dddˇ»
4974
4975 "});
4976
4977 // Adding new line
4978 cx.set_state(indoc! {"
4979 aa«a
4980 bbˇ»b
4981 "});
4982 cx.update_editor(|e, window, cx| {
4983 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4984 });
4985 cx.assert_editor_state(indoc! {"
4986 «aaa
4987 bbb
4988 added_lineˇ»
4989 "});
4990
4991 // Removing line
4992 cx.set_state(indoc! {"
4993 aa«a
4994 bbbˇ»
4995 "});
4996 cx.update_editor(|e, window, cx| {
4997 e.manipulate_immutable_lines(window, cx, |lines| {
4998 lines.pop();
4999 })
5000 });
5001 cx.assert_editor_state(indoc! {"
5002 «aaaˇ»
5003 "});
5004
5005 // Removing all lines
5006 cx.set_state(indoc! {"
5007 aa«a
5008 bbbˇ»
5009 "});
5010 cx.update_editor(|e, window, cx| {
5011 e.manipulate_immutable_lines(window, cx, |lines| {
5012 lines.drain(..);
5013 })
5014 });
5015 cx.assert_editor_state(indoc! {"
5016 ˇ
5017 "});
5018}
5019
5020#[gpui::test]
5021async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
5022 init_test(cx, |_| {});
5023
5024 let mut cx = EditorTestContext::new(cx).await;
5025
5026 // Consider continuous selection as single selection
5027 cx.set_state(indoc! {"
5028 Aaa«aa
5029 cˇ»c«c
5030 bb
5031 aaaˇ»aa
5032 "});
5033 cx.update_editor(|e, window, cx| {
5034 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
5035 });
5036 cx.assert_editor_state(indoc! {"
5037 «Aaaaa
5038 ccc
5039 bb
5040 aaaaaˇ»
5041 "});
5042
5043 cx.set_state(indoc! {"
5044 Aaa«aa
5045 cˇ»c«c
5046 bb
5047 aaaˇ»aa
5048 "});
5049 cx.update_editor(|e, window, cx| {
5050 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
5051 });
5052 cx.assert_editor_state(indoc! {"
5053 «Aaaaa
5054 ccc
5055 bbˇ»
5056 "});
5057
5058 // Consider non continuous selection as distinct dedup operations
5059 cx.set_state(indoc! {"
5060 «aaaaa
5061 bb
5062 aaaaa
5063 aaaaaˇ»
5064
5065 aaa«aaˇ»
5066 "});
5067 cx.update_editor(|e, window, cx| {
5068 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
5069 });
5070 cx.assert_editor_state(indoc! {"
5071 «aaaaa
5072 bbˇ»
5073
5074 «aaaaaˇ»
5075 "});
5076}
5077
5078#[gpui::test]
5079async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
5080 init_test(cx, |_| {});
5081
5082 let mut cx = EditorTestContext::new(cx).await;
5083
5084 cx.set_state(indoc! {"
5085 «Aaa
5086 aAa
5087 Aaaˇ»
5088 "});
5089 cx.update_editor(|e, window, cx| {
5090 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
5091 });
5092 cx.assert_editor_state(indoc! {"
5093 «Aaa
5094 aAaˇ»
5095 "});
5096
5097 cx.set_state(indoc! {"
5098 «Aaa
5099 aAa
5100 aaAˇ»
5101 "});
5102 cx.update_editor(|e, window, cx| {
5103 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
5104 });
5105 cx.assert_editor_state(indoc! {"
5106 «Aaaˇ»
5107 "});
5108}
5109
5110#[gpui::test]
5111async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
5112 init_test(cx, |_| {});
5113
5114 let mut cx = EditorTestContext::new(cx).await;
5115
5116 let js_language = Arc::new(Language::new(
5117 LanguageConfig {
5118 name: "JavaScript".into(),
5119 wrap_characters: Some(language::WrapCharactersConfig {
5120 start_prefix: "<".into(),
5121 start_suffix: ">".into(),
5122 end_prefix: "</".into(),
5123 end_suffix: ">".into(),
5124 }),
5125 ..LanguageConfig::default()
5126 },
5127 None,
5128 ));
5129
5130 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5131
5132 cx.set_state(indoc! {"
5133 «testˇ»
5134 "});
5135 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5136 cx.assert_editor_state(indoc! {"
5137 <«ˇ»>test</«ˇ»>
5138 "});
5139
5140 cx.set_state(indoc! {"
5141 «test
5142 testˇ»
5143 "});
5144 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5145 cx.assert_editor_state(indoc! {"
5146 <«ˇ»>test
5147 test</«ˇ»>
5148 "});
5149
5150 cx.set_state(indoc! {"
5151 teˇst
5152 "});
5153 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5154 cx.assert_editor_state(indoc! {"
5155 te<«ˇ»></«ˇ»>st
5156 "});
5157}
5158
5159#[gpui::test]
5160async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5161 init_test(cx, |_| {});
5162
5163 let mut cx = EditorTestContext::new(cx).await;
5164
5165 let js_language = Arc::new(Language::new(
5166 LanguageConfig {
5167 name: "JavaScript".into(),
5168 wrap_characters: Some(language::WrapCharactersConfig {
5169 start_prefix: "<".into(),
5170 start_suffix: ">".into(),
5171 end_prefix: "</".into(),
5172 end_suffix: ">".into(),
5173 }),
5174 ..LanguageConfig::default()
5175 },
5176 None,
5177 ));
5178
5179 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5180
5181 cx.set_state(indoc! {"
5182 «testˇ»
5183 «testˇ» «testˇ»
5184 «testˇ»
5185 "});
5186 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5187 cx.assert_editor_state(indoc! {"
5188 <«ˇ»>test</«ˇ»>
5189 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5190 <«ˇ»>test</«ˇ»>
5191 "});
5192
5193 cx.set_state(indoc! {"
5194 «test
5195 testˇ»
5196 «test
5197 testˇ»
5198 "});
5199 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5200 cx.assert_editor_state(indoc! {"
5201 <«ˇ»>test
5202 test</«ˇ»>
5203 <«ˇ»>test
5204 test</«ˇ»>
5205 "});
5206}
5207
5208#[gpui::test]
5209async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5210 init_test(cx, |_| {});
5211
5212 let mut cx = EditorTestContext::new(cx).await;
5213
5214 let plaintext_language = Arc::new(Language::new(
5215 LanguageConfig {
5216 name: "Plain Text".into(),
5217 ..LanguageConfig::default()
5218 },
5219 None,
5220 ));
5221
5222 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5223
5224 cx.set_state(indoc! {"
5225 «testˇ»
5226 "});
5227 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5228 cx.assert_editor_state(indoc! {"
5229 «testˇ»
5230 "});
5231}
5232
5233#[gpui::test]
5234async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5235 init_test(cx, |_| {});
5236
5237 let mut cx = EditorTestContext::new(cx).await;
5238
5239 // Manipulate with multiple selections on a single line
5240 cx.set_state(indoc! {"
5241 dd«dd
5242 cˇ»c«c
5243 bb
5244 aaaˇ»aa
5245 "});
5246 cx.update_editor(|e, window, cx| {
5247 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5248 });
5249 cx.assert_editor_state(indoc! {"
5250 «aaaaa
5251 bb
5252 ccc
5253 ddddˇ»
5254 "});
5255
5256 // Manipulate with multiple disjoin selections
5257 cx.set_state(indoc! {"
5258 5«
5259 4
5260 3
5261 2
5262 1ˇ»
5263
5264 dd«dd
5265 ccc
5266 bb
5267 aaaˇ»aa
5268 "});
5269 cx.update_editor(|e, window, cx| {
5270 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5271 });
5272 cx.assert_editor_state(indoc! {"
5273 «1
5274 2
5275 3
5276 4
5277 5ˇ»
5278
5279 «aaaaa
5280 bb
5281 ccc
5282 ddddˇ»
5283 "});
5284
5285 // Adding lines on each selection
5286 cx.set_state(indoc! {"
5287 2«
5288 1ˇ»
5289
5290 bb«bb
5291 aaaˇ»aa
5292 "});
5293 cx.update_editor(|e, window, cx| {
5294 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5295 });
5296 cx.assert_editor_state(indoc! {"
5297 «2
5298 1
5299 added lineˇ»
5300
5301 «bbbb
5302 aaaaa
5303 added lineˇ»
5304 "});
5305
5306 // Removing lines on each selection
5307 cx.set_state(indoc! {"
5308 2«
5309 1ˇ»
5310
5311 bb«bb
5312 aaaˇ»aa
5313 "});
5314 cx.update_editor(|e, window, cx| {
5315 e.manipulate_immutable_lines(window, cx, |lines| {
5316 lines.pop();
5317 })
5318 });
5319 cx.assert_editor_state(indoc! {"
5320 «2ˇ»
5321
5322 «bbbbˇ»
5323 "});
5324}
5325
5326#[gpui::test]
5327async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5328 init_test(cx, |settings| {
5329 settings.defaults.tab_size = NonZeroU32::new(3)
5330 });
5331
5332 let mut cx = EditorTestContext::new(cx).await;
5333
5334 // MULTI SELECTION
5335 // Ln.1 "«" tests empty lines
5336 // Ln.9 tests just leading whitespace
5337 cx.set_state(indoc! {"
5338 «
5339 abc // No indentationˇ»
5340 «\tabc // 1 tabˇ»
5341 \t\tabc « ˇ» // 2 tabs
5342 \t ab«c // Tab followed by space
5343 \tabc // Space followed by tab (3 spaces should be the result)
5344 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5345 abˇ»ˇc ˇ ˇ // Already space indented«
5346 \t
5347 \tabc\tdef // Only the leading tab is manipulatedˇ»
5348 "});
5349 cx.update_editor(|e, window, cx| {
5350 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5351 });
5352 cx.assert_editor_state(
5353 indoc! {"
5354 «
5355 abc // No indentation
5356 abc // 1 tab
5357 abc // 2 tabs
5358 abc // Tab followed by space
5359 abc // Space followed by tab (3 spaces should be the result)
5360 abc // Mixed indentation (tab conversion depends on the column)
5361 abc // Already space indented
5362 ·
5363 abc\tdef // Only the leading tab is manipulatedˇ»
5364 "}
5365 .replace("·", "")
5366 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5367 );
5368
5369 // Test on just a few lines, the others should remain unchanged
5370 // Only lines (3, 5, 10, 11) should change
5371 cx.set_state(
5372 indoc! {"
5373 ·
5374 abc // No indentation
5375 \tabcˇ // 1 tab
5376 \t\tabc // 2 tabs
5377 \t abcˇ // Tab followed by space
5378 \tabc // Space followed by tab (3 spaces should be the result)
5379 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5380 abc // Already space indented
5381 «\t
5382 \tabc\tdef // Only the leading tab is manipulatedˇ»
5383 "}
5384 .replace("·", "")
5385 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5386 );
5387 cx.update_editor(|e, window, cx| {
5388 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5389 });
5390 cx.assert_editor_state(
5391 indoc! {"
5392 ·
5393 abc // No indentation
5394 « abc // 1 tabˇ»
5395 \t\tabc // 2 tabs
5396 « abc // Tab followed by spaceˇ»
5397 \tabc // Space followed by tab (3 spaces should be the result)
5398 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5399 abc // Already space indented
5400 « ·
5401 abc\tdef // Only the leading tab is manipulatedˇ»
5402 "}
5403 .replace("·", "")
5404 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5405 );
5406
5407 // SINGLE SELECTION
5408 // Ln.1 "«" tests empty lines
5409 // Ln.9 tests just leading whitespace
5410 cx.set_state(indoc! {"
5411 «
5412 abc // No indentation
5413 \tabc // 1 tab
5414 \t\tabc // 2 tabs
5415 \t abc // Tab followed by space
5416 \tabc // Space followed by tab (3 spaces should be the result)
5417 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5418 abc // Already space indented
5419 \t
5420 \tabc\tdef // Only the leading tab is manipulatedˇ»
5421 "});
5422 cx.update_editor(|e, window, cx| {
5423 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5424 });
5425 cx.assert_editor_state(
5426 indoc! {"
5427 «
5428 abc // No indentation
5429 abc // 1 tab
5430 abc // 2 tabs
5431 abc // Tab followed by space
5432 abc // Space followed by tab (3 spaces should be the result)
5433 abc // Mixed indentation (tab conversion depends on the column)
5434 abc // Already space indented
5435 ·
5436 abc\tdef // Only the leading tab is manipulatedˇ»
5437 "}
5438 .replace("·", "")
5439 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5440 );
5441}
5442
5443#[gpui::test]
5444async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5445 init_test(cx, |settings| {
5446 settings.defaults.tab_size = NonZeroU32::new(3)
5447 });
5448
5449 let mut cx = EditorTestContext::new(cx).await;
5450
5451 // MULTI SELECTION
5452 // Ln.1 "«" tests empty lines
5453 // Ln.11 tests just leading whitespace
5454 cx.set_state(indoc! {"
5455 «
5456 abˇ»ˇc // No indentation
5457 abc ˇ ˇ // 1 space (< 3 so dont convert)
5458 abc « // 2 spaces (< 3 so dont convert)
5459 abc // 3 spaces (convert)
5460 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5461 «\tˇ»\t«\tˇ»abc // Already tab indented
5462 «\t abc // Tab followed by space
5463 \tabc // Space followed by tab (should be consumed due to tab)
5464 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5465 \tˇ» «\t
5466 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5467 "});
5468 cx.update_editor(|e, window, cx| {
5469 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5470 });
5471 cx.assert_editor_state(indoc! {"
5472 «
5473 abc // No indentation
5474 abc // 1 space (< 3 so dont convert)
5475 abc // 2 spaces (< 3 so dont convert)
5476 \tabc // 3 spaces (convert)
5477 \t abc // 5 spaces (1 tab + 2 spaces)
5478 \t\t\tabc // Already tab indented
5479 \t abc // Tab followed by space
5480 \tabc // Space followed by tab (should be consumed due to tab)
5481 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5482 \t\t\t
5483 \tabc \t // Only the leading spaces should be convertedˇ»
5484 "});
5485
5486 // Test on just a few lines, the other should remain unchanged
5487 // Only lines (4, 8, 11, 12) should change
5488 cx.set_state(
5489 indoc! {"
5490 ·
5491 abc // No indentation
5492 abc // 1 space (< 3 so dont convert)
5493 abc // 2 spaces (< 3 so dont convert)
5494 « abc // 3 spaces (convert)ˇ»
5495 abc // 5 spaces (1 tab + 2 spaces)
5496 \t\t\tabc // Already tab indented
5497 \t abc // Tab followed by space
5498 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5499 \t\t \tabc // Mixed indentation
5500 \t \t \t \tabc // Mixed indentation
5501 \t \tˇ
5502 « abc \t // Only the leading spaces should be convertedˇ»
5503 "}
5504 .replace("·", "")
5505 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5506 );
5507 cx.update_editor(|e, window, cx| {
5508 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5509 });
5510 cx.assert_editor_state(
5511 indoc! {"
5512 ·
5513 abc // No indentation
5514 abc // 1 space (< 3 so dont convert)
5515 abc // 2 spaces (< 3 so dont convert)
5516 «\tabc // 3 spaces (convert)ˇ»
5517 abc // 5 spaces (1 tab + 2 spaces)
5518 \t\t\tabc // Already tab indented
5519 \t abc // Tab followed by space
5520 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5521 \t\t \tabc // Mixed indentation
5522 \t \t \t \tabc // Mixed indentation
5523 «\t\t\t
5524 \tabc \t // Only the leading spaces should be convertedˇ»
5525 "}
5526 .replace("·", "")
5527 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5528 );
5529
5530 // SINGLE SELECTION
5531 // Ln.1 "«" tests empty lines
5532 // Ln.11 tests just leading whitespace
5533 cx.set_state(indoc! {"
5534 «
5535 abc // No indentation
5536 abc // 1 space (< 3 so dont convert)
5537 abc // 2 spaces (< 3 so dont convert)
5538 abc // 3 spaces (convert)
5539 abc // 5 spaces (1 tab + 2 spaces)
5540 \t\t\tabc // Already tab indented
5541 \t abc // Tab followed by space
5542 \tabc // Space followed by tab (should be consumed due to tab)
5543 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5544 \t \t
5545 abc \t // Only the leading spaces should be convertedˇ»
5546 "});
5547 cx.update_editor(|e, window, cx| {
5548 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5549 });
5550 cx.assert_editor_state(indoc! {"
5551 «
5552 abc // No indentation
5553 abc // 1 space (< 3 so dont convert)
5554 abc // 2 spaces (< 3 so dont convert)
5555 \tabc // 3 spaces (convert)
5556 \t abc // 5 spaces (1 tab + 2 spaces)
5557 \t\t\tabc // Already tab indented
5558 \t abc // Tab followed by space
5559 \tabc // Space followed by tab (should be consumed due to tab)
5560 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5561 \t\t\t
5562 \tabc \t // Only the leading spaces should be convertedˇ»
5563 "});
5564}
5565
5566#[gpui::test]
5567async fn test_toggle_case(cx: &mut TestAppContext) {
5568 init_test(cx, |_| {});
5569
5570 let mut cx = EditorTestContext::new(cx).await;
5571
5572 // If all lower case -> upper case
5573 cx.set_state(indoc! {"
5574 «hello worldˇ»
5575 "});
5576 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5577 cx.assert_editor_state(indoc! {"
5578 «HELLO WORLDˇ»
5579 "});
5580
5581 // If all upper case -> lower case
5582 cx.set_state(indoc! {"
5583 «HELLO WORLDˇ»
5584 "});
5585 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5586 cx.assert_editor_state(indoc! {"
5587 «hello worldˇ»
5588 "});
5589
5590 // If any upper case characters are identified -> lower case
5591 // This matches JetBrains IDEs
5592 cx.set_state(indoc! {"
5593 «hEllo worldˇ»
5594 "});
5595 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5596 cx.assert_editor_state(indoc! {"
5597 «hello worldˇ»
5598 "});
5599}
5600
5601#[gpui::test]
5602async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5603 init_test(cx, |_| {});
5604
5605 let mut cx = EditorTestContext::new(cx).await;
5606
5607 cx.set_state(indoc! {"
5608 «implement-windows-supportˇ»
5609 "});
5610 cx.update_editor(|e, window, cx| {
5611 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5612 });
5613 cx.assert_editor_state(indoc! {"
5614 «Implement windows supportˇ»
5615 "});
5616}
5617
5618#[gpui::test]
5619async fn test_manipulate_text(cx: &mut TestAppContext) {
5620 init_test(cx, |_| {});
5621
5622 let mut cx = EditorTestContext::new(cx).await;
5623
5624 // Test convert_to_upper_case()
5625 cx.set_state(indoc! {"
5626 «hello worldˇ»
5627 "});
5628 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5629 cx.assert_editor_state(indoc! {"
5630 «HELLO WORLDˇ»
5631 "});
5632
5633 // Test convert_to_lower_case()
5634 cx.set_state(indoc! {"
5635 «HELLO WORLDˇ»
5636 "});
5637 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5638 cx.assert_editor_state(indoc! {"
5639 «hello worldˇ»
5640 "});
5641
5642 // Test multiple line, single selection case
5643 cx.set_state(indoc! {"
5644 «The quick brown
5645 fox jumps over
5646 the lazy dogˇ»
5647 "});
5648 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5649 cx.assert_editor_state(indoc! {"
5650 «The Quick Brown
5651 Fox Jumps Over
5652 The Lazy Dogˇ»
5653 "});
5654
5655 // Test multiple line, single selection case
5656 cx.set_state(indoc! {"
5657 «The quick brown
5658 fox jumps over
5659 the lazy dogˇ»
5660 "});
5661 cx.update_editor(|e, window, cx| {
5662 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5663 });
5664 cx.assert_editor_state(indoc! {"
5665 «TheQuickBrown
5666 FoxJumpsOver
5667 TheLazyDogˇ»
5668 "});
5669
5670 // From here on out, test more complex cases of manipulate_text()
5671
5672 // Test no selection case - should affect words cursors are in
5673 // Cursor at beginning, middle, and end of word
5674 cx.set_state(indoc! {"
5675 ˇhello big beauˇtiful worldˇ
5676 "});
5677 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5678 cx.assert_editor_state(indoc! {"
5679 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5680 "});
5681
5682 // Test multiple selections on a single line and across multiple lines
5683 cx.set_state(indoc! {"
5684 «Theˇ» quick «brown
5685 foxˇ» jumps «overˇ»
5686 the «lazyˇ» dog
5687 "});
5688 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5689 cx.assert_editor_state(indoc! {"
5690 «THEˇ» quick «BROWN
5691 FOXˇ» jumps «OVERˇ»
5692 the «LAZYˇ» dog
5693 "});
5694
5695 // Test case where text length grows
5696 cx.set_state(indoc! {"
5697 «tschüߡ»
5698 "});
5699 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5700 cx.assert_editor_state(indoc! {"
5701 «TSCHÜSSˇ»
5702 "});
5703
5704 // Test to make sure we don't crash when text shrinks
5705 cx.set_state(indoc! {"
5706 aaa_bbbˇ
5707 "});
5708 cx.update_editor(|e, window, cx| {
5709 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5710 });
5711 cx.assert_editor_state(indoc! {"
5712 «aaaBbbˇ»
5713 "});
5714
5715 // Test to make sure we all aware of the fact that each word can grow and shrink
5716 // Final selections should be aware of this fact
5717 cx.set_state(indoc! {"
5718 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5719 "});
5720 cx.update_editor(|e, window, cx| {
5721 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5722 });
5723 cx.assert_editor_state(indoc! {"
5724 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5725 "});
5726
5727 cx.set_state(indoc! {"
5728 «hElLo, WoRld!ˇ»
5729 "});
5730 cx.update_editor(|e, window, cx| {
5731 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5732 });
5733 cx.assert_editor_state(indoc! {"
5734 «HeLlO, wOrLD!ˇ»
5735 "});
5736
5737 // Test selections with `line_mode() = true`.
5738 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5739 cx.set_state(indoc! {"
5740 «The quick brown
5741 fox jumps over
5742 tˇ»he lazy dog
5743 "});
5744 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5745 cx.assert_editor_state(indoc! {"
5746 «THE QUICK BROWN
5747 FOX JUMPS OVER
5748 THE LAZY DOGˇ»
5749 "});
5750}
5751
5752#[gpui::test]
5753fn test_duplicate_line(cx: &mut TestAppContext) {
5754 init_test(cx, |_| {});
5755
5756 let editor = cx.add_window(|window, cx| {
5757 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5758 build_editor(buffer, window, cx)
5759 });
5760 _ = editor.update(cx, |editor, window, cx| {
5761 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5762 s.select_display_ranges([
5763 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5764 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5765 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5766 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5767 ])
5768 });
5769 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5770 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5771 assert_eq!(
5772 display_ranges(editor, cx),
5773 vec![
5774 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5775 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5776 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5777 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5778 ]
5779 );
5780 });
5781
5782 let editor = cx.add_window(|window, cx| {
5783 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5784 build_editor(buffer, window, cx)
5785 });
5786 _ = editor.update(cx, |editor, window, cx| {
5787 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5788 s.select_display_ranges([
5789 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5790 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5791 ])
5792 });
5793 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5794 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5795 assert_eq!(
5796 display_ranges(editor, cx),
5797 vec![
5798 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5799 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5800 ]
5801 );
5802 });
5803
5804 // With `duplicate_line_up` the selections move to the duplicated lines,
5805 // which are inserted above the original lines
5806 let editor = cx.add_window(|window, cx| {
5807 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5808 build_editor(buffer, window, cx)
5809 });
5810 _ = editor.update(cx, |editor, window, cx| {
5811 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5812 s.select_display_ranges([
5813 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5814 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5815 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5816 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5817 ])
5818 });
5819 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5820 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5821 assert_eq!(
5822 display_ranges(editor, cx),
5823 vec![
5824 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5825 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5826 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5827 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0),
5828 ]
5829 );
5830 });
5831
5832 let editor = cx.add_window(|window, cx| {
5833 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5834 build_editor(buffer, window, cx)
5835 });
5836 _ = editor.update(cx, |editor, window, cx| {
5837 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5838 s.select_display_ranges([
5839 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5840 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5841 ])
5842 });
5843 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5844 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5845 assert_eq!(
5846 display_ranges(editor, cx),
5847 vec![
5848 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5849 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5850 ]
5851 );
5852 });
5853
5854 let editor = cx.add_window(|window, cx| {
5855 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5856 build_editor(buffer, window, cx)
5857 });
5858 _ = editor.update(cx, |editor, window, cx| {
5859 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5860 s.select_display_ranges([
5861 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5862 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5863 ])
5864 });
5865 editor.duplicate_selection(&DuplicateSelection, window, cx);
5866 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5867 assert_eq!(
5868 display_ranges(editor, cx),
5869 vec![
5870 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5871 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5872 ]
5873 );
5874 });
5875}
5876
5877#[gpui::test]
5878async fn test_rotate_selections(cx: &mut TestAppContext) {
5879 init_test(cx, |_| {});
5880
5881 let mut cx = EditorTestContext::new(cx).await;
5882
5883 // Rotate text selections (horizontal)
5884 cx.set_state("x=«1ˇ», y=«2ˇ», z=«3ˇ»");
5885 cx.update_editor(|e, window, cx| {
5886 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5887 });
5888 cx.assert_editor_state("x=«3ˇ», y=«1ˇ», z=«2ˇ»");
5889 cx.update_editor(|e, window, cx| {
5890 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5891 });
5892 cx.assert_editor_state("x=«1ˇ», y=«2ˇ», z=«3ˇ»");
5893
5894 // Rotate text selections (vertical)
5895 cx.set_state(indoc! {"
5896 x=«1ˇ»
5897 y=«2ˇ»
5898 z=«3ˇ»
5899 "});
5900 cx.update_editor(|e, window, cx| {
5901 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5902 });
5903 cx.assert_editor_state(indoc! {"
5904 x=«3ˇ»
5905 y=«1ˇ»
5906 z=«2ˇ»
5907 "});
5908 cx.update_editor(|e, window, cx| {
5909 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5910 });
5911 cx.assert_editor_state(indoc! {"
5912 x=«1ˇ»
5913 y=«2ˇ»
5914 z=«3ˇ»
5915 "});
5916
5917 // Rotate text selections (vertical, different lengths)
5918 cx.set_state(indoc! {"
5919 x=\"«ˇ»\"
5920 y=\"«aˇ»\"
5921 z=\"«aaˇ»\"
5922 "});
5923 cx.update_editor(|e, window, cx| {
5924 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5925 });
5926 cx.assert_editor_state(indoc! {"
5927 x=\"«aaˇ»\"
5928 y=\"«ˇ»\"
5929 z=\"«aˇ»\"
5930 "});
5931 cx.update_editor(|e, window, cx| {
5932 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5933 });
5934 cx.assert_editor_state(indoc! {"
5935 x=\"«ˇ»\"
5936 y=\"«aˇ»\"
5937 z=\"«aaˇ»\"
5938 "});
5939
5940 // Rotate whole lines (cursor positions preserved)
5941 cx.set_state(indoc! {"
5942 ˇline123
5943 liˇne23
5944 line3ˇ
5945 "});
5946 cx.update_editor(|e, window, cx| {
5947 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5948 });
5949 cx.assert_editor_state(indoc! {"
5950 line3ˇ
5951 ˇline123
5952 liˇne23
5953 "});
5954 cx.update_editor(|e, window, cx| {
5955 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5956 });
5957 cx.assert_editor_state(indoc! {"
5958 ˇline123
5959 liˇne23
5960 line3ˇ
5961 "});
5962
5963 // Rotate whole lines, multiple cursors per line (positions preserved)
5964 cx.set_state(indoc! {"
5965 ˇliˇne123
5966 ˇline23
5967 ˇline3
5968 "});
5969 cx.update_editor(|e, window, cx| {
5970 e.rotate_selections_forward(&RotateSelectionsForward, window, cx)
5971 });
5972 cx.assert_editor_state(indoc! {"
5973 ˇline3
5974 ˇliˇne123
5975 ˇline23
5976 "});
5977 cx.update_editor(|e, window, cx| {
5978 e.rotate_selections_backward(&RotateSelectionsBackward, window, cx)
5979 });
5980 cx.assert_editor_state(indoc! {"
5981 ˇliˇne123
5982 ˇline23
5983 ˇline3
5984 "});
5985}
5986
5987#[gpui::test]
5988fn test_move_line_up_down(cx: &mut TestAppContext) {
5989 init_test(cx, |_| {});
5990
5991 let editor = cx.add_window(|window, cx| {
5992 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5993 build_editor(buffer, window, cx)
5994 });
5995 _ = editor.update(cx, |editor, window, cx| {
5996 editor.fold_creases(
5997 vec![
5998 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5999 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
6000 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
6001 ],
6002 true,
6003 window,
6004 cx,
6005 );
6006 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6007 s.select_display_ranges([
6008 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6009 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
6010 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
6011 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
6012 ])
6013 });
6014 assert_eq!(
6015 editor.display_text(cx),
6016 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
6017 );
6018
6019 editor.move_line_up(&MoveLineUp, window, cx);
6020 assert_eq!(
6021 editor.display_text(cx),
6022 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
6023 );
6024 assert_eq!(
6025 display_ranges(editor, cx),
6026 vec![
6027 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
6028 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
6029 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
6030 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
6031 ]
6032 );
6033 });
6034
6035 _ = editor.update(cx, |editor, window, cx| {
6036 editor.move_line_down(&MoveLineDown, window, cx);
6037 assert_eq!(
6038 editor.display_text(cx),
6039 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
6040 );
6041 assert_eq!(
6042 display_ranges(editor, cx),
6043 vec![
6044 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6045 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
6046 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
6047 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
6048 ]
6049 );
6050 });
6051
6052 _ = editor.update(cx, |editor, window, cx| {
6053 editor.move_line_down(&MoveLineDown, window, cx);
6054 assert_eq!(
6055 editor.display_text(cx),
6056 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
6057 );
6058 assert_eq!(
6059 display_ranges(editor, cx),
6060 vec![
6061 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
6062 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
6063 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
6064 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
6065 ]
6066 );
6067 });
6068
6069 _ = editor.update(cx, |editor, window, cx| {
6070 editor.move_line_up(&MoveLineUp, window, cx);
6071 assert_eq!(
6072 editor.display_text(cx),
6073 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
6074 );
6075 assert_eq!(
6076 display_ranges(editor, cx),
6077 vec![
6078 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
6079 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
6080 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
6081 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
6082 ]
6083 );
6084 });
6085}
6086
6087#[gpui::test]
6088fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
6089 init_test(cx, |_| {});
6090 let editor = cx.add_window(|window, cx| {
6091 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
6092 build_editor(buffer, window, cx)
6093 });
6094 _ = editor.update(cx, |editor, window, cx| {
6095 editor.fold_creases(
6096 vec![Crease::simple(
6097 Point::new(6, 4)..Point::new(7, 4),
6098 FoldPlaceholder::test(),
6099 )],
6100 true,
6101 window,
6102 cx,
6103 );
6104 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6105 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
6106 });
6107 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
6108 editor.move_line_up(&MoveLineUp, window, cx);
6109 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
6110 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
6111 });
6112}
6113
6114#[gpui::test]
6115fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
6116 init_test(cx, |_| {});
6117
6118 let editor = cx.add_window(|window, cx| {
6119 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
6120 build_editor(buffer, window, cx)
6121 });
6122 _ = editor.update(cx, |editor, window, cx| {
6123 let snapshot = editor.buffer.read(cx).snapshot(cx);
6124 editor.insert_blocks(
6125 [BlockProperties {
6126 style: BlockStyle::Fixed,
6127 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
6128 height: Some(1),
6129 render: Arc::new(|_| div().into_any()),
6130 priority: 0,
6131 }],
6132 Some(Autoscroll::fit()),
6133 cx,
6134 );
6135 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6136 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
6137 });
6138 editor.move_line_down(&MoveLineDown, window, cx);
6139 });
6140}
6141
6142#[gpui::test]
6143async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
6144 init_test(cx, |_| {});
6145
6146 let mut cx = EditorTestContext::new(cx).await;
6147 cx.set_state(
6148 &"
6149 ˇzero
6150 one
6151 two
6152 three
6153 four
6154 five
6155 "
6156 .unindent(),
6157 );
6158
6159 // Create a four-line block that replaces three lines of text.
6160 cx.update_editor(|editor, window, cx| {
6161 let snapshot = editor.snapshot(window, cx);
6162 let snapshot = &snapshot.buffer_snapshot();
6163 let placement = BlockPlacement::Replace(
6164 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
6165 );
6166 editor.insert_blocks(
6167 [BlockProperties {
6168 placement,
6169 height: Some(4),
6170 style: BlockStyle::Sticky,
6171 render: Arc::new(|_| gpui::div().into_any_element()),
6172 priority: 0,
6173 }],
6174 None,
6175 cx,
6176 );
6177 });
6178
6179 // Move down so that the cursor touches the block.
6180 cx.update_editor(|editor, window, cx| {
6181 editor.move_down(&Default::default(), window, cx);
6182 });
6183 cx.assert_editor_state(
6184 &"
6185 zero
6186 «one
6187 two
6188 threeˇ»
6189 four
6190 five
6191 "
6192 .unindent(),
6193 );
6194
6195 // Move down past the block.
6196 cx.update_editor(|editor, window, cx| {
6197 editor.move_down(&Default::default(), window, cx);
6198 });
6199 cx.assert_editor_state(
6200 &"
6201 zero
6202 one
6203 two
6204 three
6205 ˇfour
6206 five
6207 "
6208 .unindent(),
6209 );
6210}
6211
6212#[gpui::test]
6213fn test_transpose(cx: &mut TestAppContext) {
6214 init_test(cx, |_| {});
6215
6216 _ = cx.add_window(|window, cx| {
6217 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
6218 editor.set_style(EditorStyle::default(), window, cx);
6219 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6220 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
6221 });
6222 editor.transpose(&Default::default(), window, cx);
6223 assert_eq!(editor.text(cx), "bac");
6224 assert_eq!(
6225 editor.selections.ranges(&editor.display_snapshot(cx)),
6226 [MultiBufferOffset(2)..MultiBufferOffset(2)]
6227 );
6228
6229 editor.transpose(&Default::default(), window, cx);
6230 assert_eq!(editor.text(cx), "bca");
6231 assert_eq!(
6232 editor.selections.ranges(&editor.display_snapshot(cx)),
6233 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6234 );
6235
6236 editor.transpose(&Default::default(), window, cx);
6237 assert_eq!(editor.text(cx), "bac");
6238 assert_eq!(
6239 editor.selections.ranges(&editor.display_snapshot(cx)),
6240 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6241 );
6242
6243 editor
6244 });
6245
6246 _ = cx.add_window(|window, cx| {
6247 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6248 editor.set_style(EditorStyle::default(), window, cx);
6249 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6250 s.select_ranges([MultiBufferOffset(3)..MultiBufferOffset(3)])
6251 });
6252 editor.transpose(&Default::default(), window, cx);
6253 assert_eq!(editor.text(cx), "acb\nde");
6254 assert_eq!(
6255 editor.selections.ranges(&editor.display_snapshot(cx)),
6256 [MultiBufferOffset(3)..MultiBufferOffset(3)]
6257 );
6258
6259 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6260 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6261 });
6262 editor.transpose(&Default::default(), window, cx);
6263 assert_eq!(editor.text(cx), "acbd\ne");
6264 assert_eq!(
6265 editor.selections.ranges(&editor.display_snapshot(cx)),
6266 [MultiBufferOffset(5)..MultiBufferOffset(5)]
6267 );
6268
6269 editor.transpose(&Default::default(), window, cx);
6270 assert_eq!(editor.text(cx), "acbde\n");
6271 assert_eq!(
6272 editor.selections.ranges(&editor.display_snapshot(cx)),
6273 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6274 );
6275
6276 editor.transpose(&Default::default(), window, cx);
6277 assert_eq!(editor.text(cx), "acbd\ne");
6278 assert_eq!(
6279 editor.selections.ranges(&editor.display_snapshot(cx)),
6280 [MultiBufferOffset(6)..MultiBufferOffset(6)]
6281 );
6282
6283 editor
6284 });
6285
6286 _ = cx.add_window(|window, cx| {
6287 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6288 editor.set_style(EditorStyle::default(), window, cx);
6289 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6290 s.select_ranges([
6291 MultiBufferOffset(1)..MultiBufferOffset(1),
6292 MultiBufferOffset(2)..MultiBufferOffset(2),
6293 MultiBufferOffset(4)..MultiBufferOffset(4),
6294 ])
6295 });
6296 editor.transpose(&Default::default(), window, cx);
6297 assert_eq!(editor.text(cx), "bacd\ne");
6298 assert_eq!(
6299 editor.selections.ranges(&editor.display_snapshot(cx)),
6300 [
6301 MultiBufferOffset(2)..MultiBufferOffset(2),
6302 MultiBufferOffset(3)..MultiBufferOffset(3),
6303 MultiBufferOffset(5)..MultiBufferOffset(5)
6304 ]
6305 );
6306
6307 editor.transpose(&Default::default(), window, cx);
6308 assert_eq!(editor.text(cx), "bcade\n");
6309 assert_eq!(
6310 editor.selections.ranges(&editor.display_snapshot(cx)),
6311 [
6312 MultiBufferOffset(3)..MultiBufferOffset(3),
6313 MultiBufferOffset(4)..MultiBufferOffset(4),
6314 MultiBufferOffset(6)..MultiBufferOffset(6)
6315 ]
6316 );
6317
6318 editor.transpose(&Default::default(), window, cx);
6319 assert_eq!(editor.text(cx), "bcda\ne");
6320 assert_eq!(
6321 editor.selections.ranges(&editor.display_snapshot(cx)),
6322 [
6323 MultiBufferOffset(4)..MultiBufferOffset(4),
6324 MultiBufferOffset(6)..MultiBufferOffset(6)
6325 ]
6326 );
6327
6328 editor.transpose(&Default::default(), window, cx);
6329 assert_eq!(editor.text(cx), "bcade\n");
6330 assert_eq!(
6331 editor.selections.ranges(&editor.display_snapshot(cx)),
6332 [
6333 MultiBufferOffset(4)..MultiBufferOffset(4),
6334 MultiBufferOffset(6)..MultiBufferOffset(6)
6335 ]
6336 );
6337
6338 editor.transpose(&Default::default(), window, cx);
6339 assert_eq!(editor.text(cx), "bcaed\n");
6340 assert_eq!(
6341 editor.selections.ranges(&editor.display_snapshot(cx)),
6342 [
6343 MultiBufferOffset(5)..MultiBufferOffset(5),
6344 MultiBufferOffset(6)..MultiBufferOffset(6)
6345 ]
6346 );
6347
6348 editor
6349 });
6350
6351 _ = cx.add_window(|window, cx| {
6352 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6353 editor.set_style(EditorStyle::default(), window, cx);
6354 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6355 s.select_ranges([MultiBufferOffset(4)..MultiBufferOffset(4)])
6356 });
6357 editor.transpose(&Default::default(), window, cx);
6358 assert_eq!(editor.text(cx), "🏀🍐✋");
6359 assert_eq!(
6360 editor.selections.ranges(&editor.display_snapshot(cx)),
6361 [MultiBufferOffset(8)..MultiBufferOffset(8)]
6362 );
6363
6364 editor.transpose(&Default::default(), window, cx);
6365 assert_eq!(editor.text(cx), "🏀✋🍐");
6366 assert_eq!(
6367 editor.selections.ranges(&editor.display_snapshot(cx)),
6368 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6369 );
6370
6371 editor.transpose(&Default::default(), window, cx);
6372 assert_eq!(editor.text(cx), "🏀🍐✋");
6373 assert_eq!(
6374 editor.selections.ranges(&editor.display_snapshot(cx)),
6375 [MultiBufferOffset(11)..MultiBufferOffset(11)]
6376 );
6377
6378 editor
6379 });
6380}
6381
6382#[gpui::test]
6383async fn test_rewrap(cx: &mut TestAppContext) {
6384 init_test(cx, |settings| {
6385 settings.languages.0.extend([
6386 (
6387 "Markdown".into(),
6388 LanguageSettingsContent {
6389 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6390 preferred_line_length: Some(40),
6391 ..Default::default()
6392 },
6393 ),
6394 (
6395 "Plain Text".into(),
6396 LanguageSettingsContent {
6397 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6398 preferred_line_length: Some(40),
6399 ..Default::default()
6400 },
6401 ),
6402 (
6403 "C++".into(),
6404 LanguageSettingsContent {
6405 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6406 preferred_line_length: Some(40),
6407 ..Default::default()
6408 },
6409 ),
6410 (
6411 "Python".into(),
6412 LanguageSettingsContent {
6413 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6414 preferred_line_length: Some(40),
6415 ..Default::default()
6416 },
6417 ),
6418 (
6419 "Rust".into(),
6420 LanguageSettingsContent {
6421 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6422 preferred_line_length: Some(40),
6423 ..Default::default()
6424 },
6425 ),
6426 ])
6427 });
6428
6429 let mut cx = EditorTestContext::new(cx).await;
6430
6431 let cpp_language = Arc::new(Language::new(
6432 LanguageConfig {
6433 name: "C++".into(),
6434 line_comments: vec!["// ".into()],
6435 ..LanguageConfig::default()
6436 },
6437 None,
6438 ));
6439 let python_language = Arc::new(Language::new(
6440 LanguageConfig {
6441 name: "Python".into(),
6442 line_comments: vec!["# ".into()],
6443 ..LanguageConfig::default()
6444 },
6445 None,
6446 ));
6447 let markdown_language = Arc::new(Language::new(
6448 LanguageConfig {
6449 name: "Markdown".into(),
6450 rewrap_prefixes: vec![
6451 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6452 regex::Regex::new("[-*+]\\s+").unwrap(),
6453 ],
6454 ..LanguageConfig::default()
6455 },
6456 None,
6457 ));
6458 let rust_language = Arc::new(
6459 Language::new(
6460 LanguageConfig {
6461 name: "Rust".into(),
6462 line_comments: vec!["// ".into(), "/// ".into()],
6463 ..LanguageConfig::default()
6464 },
6465 Some(tree_sitter_rust::LANGUAGE.into()),
6466 )
6467 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6468 .unwrap(),
6469 );
6470
6471 let plaintext_language = Arc::new(Language::new(
6472 LanguageConfig {
6473 name: "Plain Text".into(),
6474 ..LanguageConfig::default()
6475 },
6476 None,
6477 ));
6478
6479 // Test basic rewrapping of a long line with a cursor
6480 assert_rewrap(
6481 indoc! {"
6482 // ˇThis is a long comment that needs to be wrapped.
6483 "},
6484 indoc! {"
6485 // ˇThis is a long comment that needs to
6486 // be wrapped.
6487 "},
6488 cpp_language.clone(),
6489 &mut cx,
6490 );
6491
6492 // Test rewrapping a full selection
6493 assert_rewrap(
6494 indoc! {"
6495 «// This selected long comment needs to be wrapped.ˇ»"
6496 },
6497 indoc! {"
6498 «// This selected long comment needs to
6499 // be wrapped.ˇ»"
6500 },
6501 cpp_language.clone(),
6502 &mut cx,
6503 );
6504
6505 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6506 assert_rewrap(
6507 indoc! {"
6508 // ˇThis is the first line.
6509 // Thisˇ is the second line.
6510 // This is the thirdˇ line, all part of one paragraph.
6511 "},
6512 indoc! {"
6513 // ˇThis is the first line. Thisˇ is the
6514 // second line. This is the thirdˇ line,
6515 // all part of one paragraph.
6516 "},
6517 cpp_language.clone(),
6518 &mut cx,
6519 );
6520
6521 // Test multiple cursors in different paragraphs trigger separate rewraps
6522 assert_rewrap(
6523 indoc! {"
6524 // ˇThis is the first paragraph, first line.
6525 // ˇThis is the first paragraph, second line.
6526
6527 // ˇThis is the second paragraph, first line.
6528 // ˇThis is the second paragraph, second line.
6529 "},
6530 indoc! {"
6531 // ˇThis is the first paragraph, first
6532 // line. ˇThis is the first paragraph,
6533 // second line.
6534
6535 // ˇThis is the second paragraph, first
6536 // line. ˇThis is the second paragraph,
6537 // second line.
6538 "},
6539 cpp_language.clone(),
6540 &mut cx,
6541 );
6542
6543 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6544 assert_rewrap(
6545 indoc! {"
6546 «// A regular long long comment to be wrapped.
6547 /// A documentation long comment to be wrapped.ˇ»
6548 "},
6549 indoc! {"
6550 «// A regular long long comment to be
6551 // wrapped.
6552 /// A documentation long comment to be
6553 /// wrapped.ˇ»
6554 "},
6555 rust_language.clone(),
6556 &mut cx,
6557 );
6558
6559 // Test that change in indentation level trigger seperate rewraps
6560 assert_rewrap(
6561 indoc! {"
6562 fn foo() {
6563 «// This is a long comment at the base indent.
6564 // This is a long comment at the next indent.ˇ»
6565 }
6566 "},
6567 indoc! {"
6568 fn foo() {
6569 «// This is a long comment at the
6570 // base indent.
6571 // This is a long comment at the
6572 // next indent.ˇ»
6573 }
6574 "},
6575 rust_language.clone(),
6576 &mut cx,
6577 );
6578
6579 // Test that different comment prefix characters (e.g., '#') are handled correctly
6580 assert_rewrap(
6581 indoc! {"
6582 # ˇThis is a long comment using a pound sign.
6583 "},
6584 indoc! {"
6585 # ˇThis is a long comment using a pound
6586 # sign.
6587 "},
6588 python_language,
6589 &mut cx,
6590 );
6591
6592 // Test rewrapping only affects comments, not code even when selected
6593 assert_rewrap(
6594 indoc! {"
6595 «/// This doc comment is long and should be wrapped.
6596 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6597 "},
6598 indoc! {"
6599 «/// This doc comment is long and should
6600 /// be wrapped.
6601 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6602 "},
6603 rust_language.clone(),
6604 &mut cx,
6605 );
6606
6607 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6608 assert_rewrap(
6609 indoc! {"
6610 # Header
6611
6612 A long long long line of markdown text to wrap.ˇ
6613 "},
6614 indoc! {"
6615 # Header
6616
6617 A long long long line of markdown text
6618 to wrap.ˇ
6619 "},
6620 markdown_language.clone(),
6621 &mut cx,
6622 );
6623
6624 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6625 assert_rewrap(
6626 indoc! {"
6627 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6628 2. This is a numbered list item that is very long and needs to be wrapped properly.
6629 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6630 "},
6631 indoc! {"
6632 «1. This is a numbered list item that is
6633 very long and needs to be wrapped
6634 properly.
6635 2. This is a numbered list item that is
6636 very long and needs to be wrapped
6637 properly.
6638 - This is an unordered list item that is
6639 also very long and should not merge
6640 with the numbered item.ˇ»
6641 "},
6642 markdown_language.clone(),
6643 &mut cx,
6644 );
6645
6646 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6647 assert_rewrap(
6648 indoc! {"
6649 «1. This is a numbered list item that is
6650 very long and needs to be wrapped
6651 properly.
6652 2. This is a numbered list item that is
6653 very long and needs to be wrapped
6654 properly.
6655 - This is an unordered list item that is
6656 also very long and should not merge with
6657 the numbered item.ˇ»
6658 "},
6659 indoc! {"
6660 «1. This is a numbered list item that is
6661 very long and needs to be wrapped
6662 properly.
6663 2. This is a numbered list item that is
6664 very long and needs to be wrapped
6665 properly.
6666 - This is an unordered list item that is
6667 also very long and should not merge
6668 with the numbered item.ˇ»
6669 "},
6670 markdown_language.clone(),
6671 &mut cx,
6672 );
6673
6674 // Test that rewrapping maintain indents even when they already exists.
6675 assert_rewrap(
6676 indoc! {"
6677 «1. This is a numbered list
6678 item that is very long and needs to be wrapped properly.
6679 2. This is a numbered list
6680 item that is very long and needs to be wrapped properly.
6681 - This is an unordered list item that is also very long and
6682 should not merge with the numbered item.ˇ»
6683 "},
6684 indoc! {"
6685 «1. This is a numbered list item that is
6686 very long and needs to be wrapped
6687 properly.
6688 2. This is a numbered list item that is
6689 very long and needs to be wrapped
6690 properly.
6691 - This is an unordered list item that is
6692 also very long and should not merge
6693 with the numbered item.ˇ»
6694 "},
6695 markdown_language,
6696 &mut cx,
6697 );
6698
6699 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6700 assert_rewrap(
6701 indoc! {"
6702 ˇThis is a very long line of plain text that will be wrapped.
6703 "},
6704 indoc! {"
6705 ˇThis is a very long line of plain text
6706 that will be wrapped.
6707 "},
6708 plaintext_language.clone(),
6709 &mut cx,
6710 );
6711
6712 // Test that non-commented code acts as a paragraph boundary within a selection
6713 assert_rewrap(
6714 indoc! {"
6715 «// This is the first long comment block to be wrapped.
6716 fn my_func(a: u32);
6717 // This is the second long comment block to be wrapped.ˇ»
6718 "},
6719 indoc! {"
6720 «// This is the first long comment block
6721 // to be wrapped.
6722 fn my_func(a: u32);
6723 // This is the second long comment block
6724 // to be wrapped.ˇ»
6725 "},
6726 rust_language,
6727 &mut cx,
6728 );
6729
6730 // Test rewrapping multiple selections, including ones with blank lines or tabs
6731 assert_rewrap(
6732 indoc! {"
6733 «ˇThis is a very long line that will be wrapped.
6734
6735 This is another paragraph in the same selection.»
6736
6737 «\tThis is a very long indented line that will be wrapped.ˇ»
6738 "},
6739 indoc! {"
6740 «ˇThis is a very long line that will be
6741 wrapped.
6742
6743 This is another paragraph in the same
6744 selection.»
6745
6746 «\tThis is a very long indented line
6747 \tthat will be wrapped.ˇ»
6748 "},
6749 plaintext_language,
6750 &mut cx,
6751 );
6752
6753 // Test that an empty comment line acts as a paragraph boundary
6754 assert_rewrap(
6755 indoc! {"
6756 // ˇThis is a long comment that will be wrapped.
6757 //
6758 // And this is another long comment that will also be wrapped.ˇ
6759 "},
6760 indoc! {"
6761 // ˇThis is a long comment that will be
6762 // wrapped.
6763 //
6764 // And this is another long comment that
6765 // will also be wrapped.ˇ
6766 "},
6767 cpp_language,
6768 &mut cx,
6769 );
6770
6771 #[track_caller]
6772 fn assert_rewrap(
6773 unwrapped_text: &str,
6774 wrapped_text: &str,
6775 language: Arc<Language>,
6776 cx: &mut EditorTestContext,
6777 ) {
6778 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6779 cx.set_state(unwrapped_text);
6780 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6781 cx.assert_editor_state(wrapped_text);
6782 }
6783}
6784
6785#[gpui::test]
6786async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6787 init_test(cx, |settings| {
6788 settings.languages.0.extend([(
6789 "Rust".into(),
6790 LanguageSettingsContent {
6791 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6792 preferred_line_length: Some(40),
6793 ..Default::default()
6794 },
6795 )])
6796 });
6797
6798 let mut cx = EditorTestContext::new(cx).await;
6799
6800 let rust_lang = Arc::new(
6801 Language::new(
6802 LanguageConfig {
6803 name: "Rust".into(),
6804 line_comments: vec!["// ".into()],
6805 block_comment: Some(BlockCommentConfig {
6806 start: "/*".into(),
6807 end: "*/".into(),
6808 prefix: "* ".into(),
6809 tab_size: 1,
6810 }),
6811 documentation_comment: Some(BlockCommentConfig {
6812 start: "/**".into(),
6813 end: "*/".into(),
6814 prefix: "* ".into(),
6815 tab_size: 1,
6816 }),
6817
6818 ..LanguageConfig::default()
6819 },
6820 Some(tree_sitter_rust::LANGUAGE.into()),
6821 )
6822 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6823 .unwrap(),
6824 );
6825
6826 // regular block comment
6827 assert_rewrap(
6828 indoc! {"
6829 /*
6830 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6831 */
6832 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6833 "},
6834 indoc! {"
6835 /*
6836 *ˇ Lorem ipsum dolor sit amet,
6837 * consectetur adipiscing elit.
6838 */
6839 /*
6840 *ˇ Lorem ipsum dolor sit amet,
6841 * consectetur adipiscing elit.
6842 */
6843 "},
6844 rust_lang.clone(),
6845 &mut cx,
6846 );
6847
6848 // indent is respected
6849 assert_rewrap(
6850 indoc! {"
6851 {}
6852 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6853 "},
6854 indoc! {"
6855 {}
6856 /*
6857 *ˇ Lorem ipsum dolor sit amet,
6858 * consectetur adipiscing elit.
6859 */
6860 "},
6861 rust_lang.clone(),
6862 &mut cx,
6863 );
6864
6865 // short block comments with inline delimiters
6866 assert_rewrap(
6867 indoc! {"
6868 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6869 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6870 */
6871 /*
6872 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6873 "},
6874 indoc! {"
6875 /*
6876 *ˇ Lorem ipsum dolor sit amet,
6877 * consectetur adipiscing elit.
6878 */
6879 /*
6880 *ˇ Lorem ipsum dolor sit amet,
6881 * consectetur adipiscing elit.
6882 */
6883 /*
6884 *ˇ Lorem ipsum dolor sit amet,
6885 * consectetur adipiscing elit.
6886 */
6887 "},
6888 rust_lang.clone(),
6889 &mut cx,
6890 );
6891
6892 // multiline block comment with inline start/end delimiters
6893 assert_rewrap(
6894 indoc! {"
6895 /*ˇ Lorem ipsum dolor sit amet,
6896 * consectetur adipiscing elit. */
6897 "},
6898 indoc! {"
6899 /*
6900 *ˇ Lorem ipsum dolor sit amet,
6901 * consectetur adipiscing elit.
6902 */
6903 "},
6904 rust_lang.clone(),
6905 &mut cx,
6906 );
6907
6908 // block comment rewrap still respects paragraph bounds
6909 assert_rewrap(
6910 indoc! {"
6911 /*
6912 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6913 *
6914 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6915 */
6916 "},
6917 indoc! {"
6918 /*
6919 *ˇ Lorem ipsum dolor sit amet,
6920 * consectetur adipiscing elit.
6921 *
6922 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6923 */
6924 "},
6925 rust_lang.clone(),
6926 &mut cx,
6927 );
6928
6929 // documentation comments
6930 assert_rewrap(
6931 indoc! {"
6932 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6933 /**
6934 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6935 */
6936 "},
6937 indoc! {"
6938 /**
6939 *ˇ Lorem ipsum dolor sit amet,
6940 * consectetur adipiscing elit.
6941 */
6942 /**
6943 *ˇ Lorem ipsum dolor sit amet,
6944 * consectetur adipiscing elit.
6945 */
6946 "},
6947 rust_lang.clone(),
6948 &mut cx,
6949 );
6950
6951 // different, adjacent comments
6952 assert_rewrap(
6953 indoc! {"
6954 /**
6955 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6956 */
6957 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6958 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6959 "},
6960 indoc! {"
6961 /**
6962 *ˇ Lorem ipsum dolor sit amet,
6963 * consectetur adipiscing elit.
6964 */
6965 /*
6966 *ˇ Lorem ipsum dolor sit amet,
6967 * consectetur adipiscing elit.
6968 */
6969 //ˇ Lorem ipsum dolor sit amet,
6970 // consectetur adipiscing elit.
6971 "},
6972 rust_lang.clone(),
6973 &mut cx,
6974 );
6975
6976 // selection w/ single short block comment
6977 assert_rewrap(
6978 indoc! {"
6979 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6980 "},
6981 indoc! {"
6982 «/*
6983 * Lorem ipsum dolor sit amet,
6984 * consectetur adipiscing elit.
6985 */ˇ»
6986 "},
6987 rust_lang.clone(),
6988 &mut cx,
6989 );
6990
6991 // rewrapping a single comment w/ abutting comments
6992 assert_rewrap(
6993 indoc! {"
6994 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6995 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6996 "},
6997 indoc! {"
6998 /*
6999 * ˇLorem ipsum dolor sit amet,
7000 * consectetur adipiscing elit.
7001 */
7002 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7003 "},
7004 rust_lang.clone(),
7005 &mut cx,
7006 );
7007
7008 // selection w/ non-abutting short block comments
7009 assert_rewrap(
7010 indoc! {"
7011 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7012
7013 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
7014 "},
7015 indoc! {"
7016 «/*
7017 * Lorem ipsum dolor sit amet,
7018 * consectetur adipiscing elit.
7019 */
7020
7021 /*
7022 * Lorem ipsum dolor sit amet,
7023 * consectetur adipiscing elit.
7024 */ˇ»
7025 "},
7026 rust_lang.clone(),
7027 &mut cx,
7028 );
7029
7030 // selection of multiline block comments
7031 assert_rewrap(
7032 indoc! {"
7033 «/* Lorem ipsum dolor sit amet,
7034 * consectetur adipiscing elit. */ˇ»
7035 "},
7036 indoc! {"
7037 «/*
7038 * Lorem ipsum dolor sit amet,
7039 * consectetur adipiscing elit.
7040 */ˇ»
7041 "},
7042 rust_lang.clone(),
7043 &mut cx,
7044 );
7045
7046 // partial selection of multiline block comments
7047 assert_rewrap(
7048 indoc! {"
7049 «/* Lorem ipsum dolor sit amet,ˇ»
7050 * consectetur adipiscing elit. */
7051 /* Lorem ipsum dolor sit amet,
7052 «* consectetur adipiscing elit. */ˇ»
7053 "},
7054 indoc! {"
7055 «/*
7056 * Lorem ipsum dolor sit amet,ˇ»
7057 * consectetur adipiscing elit. */
7058 /* Lorem ipsum dolor sit amet,
7059 «* consectetur adipiscing elit.
7060 */ˇ»
7061 "},
7062 rust_lang.clone(),
7063 &mut cx,
7064 );
7065
7066 // selection w/ abutting short block comments
7067 // TODO: should not be combined; should rewrap as 2 comments
7068 assert_rewrap(
7069 indoc! {"
7070 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7071 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
7072 "},
7073 // desired behavior:
7074 // indoc! {"
7075 // «/*
7076 // * Lorem ipsum dolor sit amet,
7077 // * consectetur adipiscing elit.
7078 // */
7079 // /*
7080 // * Lorem ipsum dolor sit amet,
7081 // * consectetur adipiscing elit.
7082 // */ˇ»
7083 // "},
7084 // actual behaviour:
7085 indoc! {"
7086 «/*
7087 * Lorem ipsum dolor sit amet,
7088 * consectetur adipiscing elit. Lorem
7089 * ipsum dolor sit amet, consectetur
7090 * adipiscing elit.
7091 */ˇ»
7092 "},
7093 rust_lang.clone(),
7094 &mut cx,
7095 );
7096
7097 // TODO: same as above, but with delimiters on separate line
7098 // assert_rewrap(
7099 // indoc! {"
7100 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7101 // */
7102 // /*
7103 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
7104 // "},
7105 // // desired:
7106 // // indoc! {"
7107 // // «/*
7108 // // * Lorem ipsum dolor sit amet,
7109 // // * consectetur adipiscing elit.
7110 // // */
7111 // // /*
7112 // // * Lorem ipsum dolor sit amet,
7113 // // * consectetur adipiscing elit.
7114 // // */ˇ»
7115 // // "},
7116 // // actual: (but with trailing w/s on the empty lines)
7117 // indoc! {"
7118 // «/*
7119 // * Lorem ipsum dolor sit amet,
7120 // * consectetur adipiscing elit.
7121 // *
7122 // */
7123 // /*
7124 // *
7125 // * Lorem ipsum dolor sit amet,
7126 // * consectetur adipiscing elit.
7127 // */ˇ»
7128 // "},
7129 // rust_lang.clone(),
7130 // &mut cx,
7131 // );
7132
7133 // TODO these are unhandled edge cases; not correct, just documenting known issues
7134 assert_rewrap(
7135 indoc! {"
7136 /*
7137 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
7138 */
7139 /*
7140 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
7141 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
7142 "},
7143 // desired:
7144 // indoc! {"
7145 // /*
7146 // *ˇ Lorem ipsum dolor sit amet,
7147 // * consectetur adipiscing elit.
7148 // */
7149 // /*
7150 // *ˇ Lorem ipsum dolor sit amet,
7151 // * consectetur adipiscing elit.
7152 // */
7153 // /*
7154 // *ˇ Lorem ipsum dolor sit amet
7155 // */ /* consectetur adipiscing elit. */
7156 // "},
7157 // actual:
7158 indoc! {"
7159 /*
7160 //ˇ Lorem ipsum dolor sit amet,
7161 // consectetur adipiscing elit.
7162 */
7163 /*
7164 * //ˇ Lorem ipsum dolor sit amet,
7165 * consectetur adipiscing elit.
7166 */
7167 /*
7168 *ˇ Lorem ipsum dolor sit amet */ /*
7169 * consectetur adipiscing elit.
7170 */
7171 "},
7172 rust_lang,
7173 &mut cx,
7174 );
7175
7176 #[track_caller]
7177 fn assert_rewrap(
7178 unwrapped_text: &str,
7179 wrapped_text: &str,
7180 language: Arc<Language>,
7181 cx: &mut EditorTestContext,
7182 ) {
7183 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
7184 cx.set_state(unwrapped_text);
7185 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
7186 cx.assert_editor_state(wrapped_text);
7187 }
7188}
7189
7190#[gpui::test]
7191async fn test_hard_wrap(cx: &mut TestAppContext) {
7192 init_test(cx, |_| {});
7193 let mut cx = EditorTestContext::new(cx).await;
7194
7195 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
7196 cx.update_editor(|editor, _, cx| {
7197 editor.set_hard_wrap(Some(14), cx);
7198 });
7199
7200 cx.set_state(indoc!(
7201 "
7202 one two three ˇ
7203 "
7204 ));
7205 cx.simulate_input("four");
7206 cx.run_until_parked();
7207
7208 cx.assert_editor_state(indoc!(
7209 "
7210 one two three
7211 fourˇ
7212 "
7213 ));
7214
7215 cx.update_editor(|editor, window, cx| {
7216 editor.newline(&Default::default(), window, cx);
7217 });
7218 cx.run_until_parked();
7219 cx.assert_editor_state(indoc!(
7220 "
7221 one two three
7222 four
7223 ˇ
7224 "
7225 ));
7226
7227 cx.simulate_input("five");
7228 cx.run_until_parked();
7229 cx.assert_editor_state(indoc!(
7230 "
7231 one two three
7232 four
7233 fiveˇ
7234 "
7235 ));
7236
7237 cx.update_editor(|editor, window, cx| {
7238 editor.newline(&Default::default(), window, cx);
7239 });
7240 cx.run_until_parked();
7241 cx.simulate_input("# ");
7242 cx.run_until_parked();
7243 cx.assert_editor_state(indoc!(
7244 "
7245 one two three
7246 four
7247 five
7248 # ˇ
7249 "
7250 ));
7251
7252 cx.update_editor(|editor, window, cx| {
7253 editor.newline(&Default::default(), window, cx);
7254 });
7255 cx.run_until_parked();
7256 cx.assert_editor_state(indoc!(
7257 "
7258 one two three
7259 four
7260 five
7261 #\x20
7262 #ˇ
7263 "
7264 ));
7265
7266 cx.simulate_input(" 6");
7267 cx.run_until_parked();
7268 cx.assert_editor_state(indoc!(
7269 "
7270 one two three
7271 four
7272 five
7273 #
7274 # 6ˇ
7275 "
7276 ));
7277}
7278
7279#[gpui::test]
7280async fn test_cut_line_ends(cx: &mut TestAppContext) {
7281 init_test(cx, |_| {});
7282
7283 let mut cx = EditorTestContext::new(cx).await;
7284
7285 cx.set_state(indoc! {"The quick brownˇ"});
7286 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7287 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7288
7289 cx.set_state(indoc! {"The emacs foxˇ"});
7290 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7291 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7292
7293 cx.set_state(indoc! {"
7294 The quick« brownˇ»
7295 fox jumps overˇ
7296 the lazy dog"});
7297 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7298 cx.assert_editor_state(indoc! {"
7299 The quickˇ
7300 ˇthe lazy dog"});
7301
7302 cx.set_state(indoc! {"
7303 The quick« brownˇ»
7304 fox jumps overˇ
7305 the lazy dog"});
7306 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7307 cx.assert_editor_state(indoc! {"
7308 The quickˇ
7309 fox jumps overˇthe lazy dog"});
7310
7311 cx.set_state(indoc! {"
7312 The quick« brownˇ»
7313 fox jumps overˇ
7314 the lazy dog"});
7315 cx.update_editor(|e, window, cx| {
7316 e.cut_to_end_of_line(
7317 &CutToEndOfLine {
7318 stop_at_newlines: true,
7319 },
7320 window,
7321 cx,
7322 )
7323 });
7324 cx.assert_editor_state(indoc! {"
7325 The quickˇ
7326 fox jumps overˇ
7327 the lazy dog"});
7328
7329 cx.set_state(indoc! {"
7330 The quick« brownˇ»
7331 fox jumps overˇ
7332 the lazy dog"});
7333 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7334 cx.assert_editor_state(indoc! {"
7335 The quickˇ
7336 fox jumps overˇthe lazy dog"});
7337}
7338
7339#[gpui::test]
7340async fn test_clipboard(cx: &mut TestAppContext) {
7341 init_test(cx, |_| {});
7342
7343 let mut cx = EditorTestContext::new(cx).await;
7344
7345 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7346 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7347 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7348
7349 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7350 cx.set_state("two ˇfour ˇsix ˇ");
7351 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7352 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7353
7354 // Paste again but with only two cursors. Since the number of cursors doesn't
7355 // match the number of slices in the clipboard, the entire clipboard text
7356 // is pasted at each cursor.
7357 cx.set_state("ˇtwo one✅ four three six five ˇ");
7358 cx.update_editor(|e, window, cx| {
7359 e.handle_input("( ", window, cx);
7360 e.paste(&Paste, window, cx);
7361 e.handle_input(") ", window, cx);
7362 });
7363 cx.assert_editor_state(
7364 &([
7365 "( one✅ ",
7366 "three ",
7367 "five ) ˇtwo one✅ four three six five ( one✅ ",
7368 "three ",
7369 "five ) ˇ",
7370 ]
7371 .join("\n")),
7372 );
7373
7374 // Cut with three selections, one of which is full-line.
7375 cx.set_state(indoc! {"
7376 1«2ˇ»3
7377 4ˇ567
7378 «8ˇ»9"});
7379 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7380 cx.assert_editor_state(indoc! {"
7381 1ˇ3
7382 ˇ9"});
7383
7384 // Paste with three selections, noticing how the copied selection that was full-line
7385 // gets inserted before the second cursor.
7386 cx.set_state(indoc! {"
7387 1ˇ3
7388 9ˇ
7389 «oˇ»ne"});
7390 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7391 cx.assert_editor_state(indoc! {"
7392 12ˇ3
7393 4567
7394 9ˇ
7395 8ˇne"});
7396
7397 // Copy with a single cursor only, which writes the whole line into the clipboard.
7398 cx.set_state(indoc! {"
7399 The quick brown
7400 fox juˇmps over
7401 the lazy dog"});
7402 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7403 assert_eq!(
7404 cx.read_from_clipboard()
7405 .and_then(|item| item.text().as_deref().map(str::to_string)),
7406 Some("fox jumps over\n".to_string())
7407 );
7408
7409 // Paste with three selections, noticing how the copied full-line selection is inserted
7410 // before the empty selections but replaces the selection that is non-empty.
7411 cx.set_state(indoc! {"
7412 Tˇhe quick brown
7413 «foˇ»x jumps over
7414 tˇhe lazy dog"});
7415 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7416 cx.assert_editor_state(indoc! {"
7417 fox jumps over
7418 Tˇhe quick brown
7419 fox jumps over
7420 ˇx jumps over
7421 fox jumps over
7422 tˇhe lazy dog"});
7423}
7424
7425#[gpui::test]
7426async fn test_copy_trim(cx: &mut TestAppContext) {
7427 init_test(cx, |_| {});
7428
7429 let mut cx = EditorTestContext::new(cx).await;
7430 cx.set_state(
7431 r#" «for selection in selections.iter() {
7432 let mut start = selection.start;
7433 let mut end = selection.end;
7434 let is_entire_line = selection.is_empty();
7435 if is_entire_line {
7436 start = Point::new(start.row, 0);ˇ»
7437 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7438 }
7439 "#,
7440 );
7441 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7442 assert_eq!(
7443 cx.read_from_clipboard()
7444 .and_then(|item| item.text().as_deref().map(str::to_string)),
7445 Some(
7446 "for selection in selections.iter() {
7447 let mut start = selection.start;
7448 let mut end = selection.end;
7449 let is_entire_line = selection.is_empty();
7450 if is_entire_line {
7451 start = Point::new(start.row, 0);"
7452 .to_string()
7453 ),
7454 "Regular copying preserves all indentation selected",
7455 );
7456 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7457 assert_eq!(
7458 cx.read_from_clipboard()
7459 .and_then(|item| item.text().as_deref().map(str::to_string)),
7460 Some(
7461 "for selection in selections.iter() {
7462let mut start = selection.start;
7463let mut end = selection.end;
7464let is_entire_line = selection.is_empty();
7465if is_entire_line {
7466 start = Point::new(start.row, 0);"
7467 .to_string()
7468 ),
7469 "Copying with stripping should strip all leading whitespaces"
7470 );
7471
7472 cx.set_state(
7473 r#" « for selection in selections.iter() {
7474 let mut start = selection.start;
7475 let mut end = selection.end;
7476 let is_entire_line = selection.is_empty();
7477 if is_entire_line {
7478 start = Point::new(start.row, 0);ˇ»
7479 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7480 }
7481 "#,
7482 );
7483 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7484 assert_eq!(
7485 cx.read_from_clipboard()
7486 .and_then(|item| item.text().as_deref().map(str::to_string)),
7487 Some(
7488 " for selection in selections.iter() {
7489 let mut start = selection.start;
7490 let mut end = selection.end;
7491 let is_entire_line = selection.is_empty();
7492 if is_entire_line {
7493 start = Point::new(start.row, 0);"
7494 .to_string()
7495 ),
7496 "Regular copying preserves all indentation selected",
7497 );
7498 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7499 assert_eq!(
7500 cx.read_from_clipboard()
7501 .and_then(|item| item.text().as_deref().map(str::to_string)),
7502 Some(
7503 "for selection in selections.iter() {
7504let mut start = selection.start;
7505let mut end = selection.end;
7506let is_entire_line = selection.is_empty();
7507if is_entire_line {
7508 start = Point::new(start.row, 0);"
7509 .to_string()
7510 ),
7511 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7512 );
7513
7514 cx.set_state(
7515 r#" «ˇ for selection in selections.iter() {
7516 let mut start = selection.start;
7517 let mut end = selection.end;
7518 let is_entire_line = selection.is_empty();
7519 if is_entire_line {
7520 start = Point::new(start.row, 0);»
7521 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7522 }
7523 "#,
7524 );
7525 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7526 assert_eq!(
7527 cx.read_from_clipboard()
7528 .and_then(|item| item.text().as_deref().map(str::to_string)),
7529 Some(
7530 " for selection in selections.iter() {
7531 let mut start = selection.start;
7532 let mut end = selection.end;
7533 let is_entire_line = selection.is_empty();
7534 if is_entire_line {
7535 start = Point::new(start.row, 0);"
7536 .to_string()
7537 ),
7538 "Regular copying for reverse selection works the same",
7539 );
7540 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7541 assert_eq!(
7542 cx.read_from_clipboard()
7543 .and_then(|item| item.text().as_deref().map(str::to_string)),
7544 Some(
7545 "for selection in selections.iter() {
7546let mut start = selection.start;
7547let mut end = selection.end;
7548let is_entire_line = selection.is_empty();
7549if is_entire_line {
7550 start = Point::new(start.row, 0);"
7551 .to_string()
7552 ),
7553 "Copying with stripping for reverse selection works the same"
7554 );
7555
7556 cx.set_state(
7557 r#" for selection «in selections.iter() {
7558 let mut start = selection.start;
7559 let mut end = selection.end;
7560 let is_entire_line = selection.is_empty();
7561 if is_entire_line {
7562 start = Point::new(start.row, 0);ˇ»
7563 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7564 }
7565 "#,
7566 );
7567 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7568 assert_eq!(
7569 cx.read_from_clipboard()
7570 .and_then(|item| item.text().as_deref().map(str::to_string)),
7571 Some(
7572 "in selections.iter() {
7573 let mut start = selection.start;
7574 let mut end = selection.end;
7575 let is_entire_line = selection.is_empty();
7576 if is_entire_line {
7577 start = Point::new(start.row, 0);"
7578 .to_string()
7579 ),
7580 "When selecting past the indent, the copying works as usual",
7581 );
7582 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7583 assert_eq!(
7584 cx.read_from_clipboard()
7585 .and_then(|item| item.text().as_deref().map(str::to_string)),
7586 Some(
7587 "in selections.iter() {
7588 let mut start = selection.start;
7589 let mut end = selection.end;
7590 let is_entire_line = selection.is_empty();
7591 if is_entire_line {
7592 start = Point::new(start.row, 0);"
7593 .to_string()
7594 ),
7595 "When selecting past the indent, nothing is trimmed"
7596 );
7597
7598 cx.set_state(
7599 r#" «for selection in selections.iter() {
7600 let mut start = selection.start;
7601
7602 let mut end = selection.end;
7603 let is_entire_line = selection.is_empty();
7604 if is_entire_line {
7605 start = Point::new(start.row, 0);
7606ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7607 }
7608 "#,
7609 );
7610 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7611 assert_eq!(
7612 cx.read_from_clipboard()
7613 .and_then(|item| item.text().as_deref().map(str::to_string)),
7614 Some(
7615 "for selection in selections.iter() {
7616let mut start = selection.start;
7617
7618let mut end = selection.end;
7619let is_entire_line = selection.is_empty();
7620if is_entire_line {
7621 start = Point::new(start.row, 0);
7622"
7623 .to_string()
7624 ),
7625 "Copying with stripping should ignore empty lines"
7626 );
7627}
7628
7629#[gpui::test]
7630async fn test_copy_trim_line_mode(cx: &mut TestAppContext) {
7631 init_test(cx, |_| {});
7632
7633 let mut cx = EditorTestContext::new(cx).await;
7634
7635 cx.set_state(indoc! {"
7636 « a
7637 bˇ»
7638 "});
7639 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
7640 cx.update_editor(|editor, window, cx| editor.copy_and_trim(&CopyAndTrim, window, cx));
7641
7642 assert_eq!(
7643 cx.read_from_clipboard().and_then(|item| item.text()),
7644 Some("a\nb\n".to_string())
7645 );
7646}
7647
7648#[gpui::test]
7649async fn test_clipboard_line_numbers_from_multibuffer(cx: &mut TestAppContext) {
7650 init_test(cx, |_| {});
7651
7652 let fs = FakeFs::new(cx.executor());
7653 fs.insert_file(
7654 path!("/file.txt"),
7655 "first line\nsecond line\nthird line\nfourth line\nfifth line\n".into(),
7656 )
7657 .await;
7658
7659 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
7660
7661 let buffer = project
7662 .update(cx, |project, cx| {
7663 project.open_local_buffer(path!("/file.txt"), cx)
7664 })
7665 .await
7666 .unwrap();
7667
7668 let multibuffer = cx.new(|cx| {
7669 let mut multibuffer = MultiBuffer::new(ReadWrite);
7670 multibuffer.push_excerpts(
7671 buffer.clone(),
7672 [ExcerptRange::new(Point::new(2, 0)..Point::new(5, 0))],
7673 cx,
7674 );
7675 multibuffer
7676 });
7677
7678 let (editor, cx) = cx.add_window_view(|window, cx| {
7679 build_editor_with_project(project.clone(), multibuffer, window, cx)
7680 });
7681
7682 editor.update_in(cx, |editor, window, cx| {
7683 assert_eq!(editor.text(cx), "third line\nfourth line\nfifth line\n");
7684
7685 editor.select_all(&SelectAll, window, cx);
7686 editor.copy(&Copy, window, cx);
7687 });
7688
7689 let clipboard_selections: Option<Vec<ClipboardSelection>> = cx
7690 .read_from_clipboard()
7691 .and_then(|item| item.entries().first().cloned())
7692 .and_then(|entry| match entry {
7693 gpui::ClipboardEntry::String(text) => text.metadata_json(),
7694 _ => None,
7695 });
7696
7697 let selections = clipboard_selections.expect("should have clipboard selections");
7698 assert_eq!(selections.len(), 1);
7699 let selection = &selections[0];
7700 assert_eq!(
7701 selection.line_range,
7702 Some(2..=5),
7703 "line range should be from original file (rows 2-5), not multibuffer rows (0-2)"
7704 );
7705}
7706
7707#[gpui::test]
7708async fn test_paste_multiline(cx: &mut TestAppContext) {
7709 init_test(cx, |_| {});
7710
7711 let mut cx = EditorTestContext::new(cx).await;
7712 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7713
7714 // Cut an indented block, without the leading whitespace.
7715 cx.set_state(indoc! {"
7716 const a: B = (
7717 c(),
7718 «d(
7719 e,
7720 f
7721 )ˇ»
7722 );
7723 "});
7724 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7725 cx.assert_editor_state(indoc! {"
7726 const a: B = (
7727 c(),
7728 ˇ
7729 );
7730 "});
7731
7732 // Paste it at the same position.
7733 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7734 cx.assert_editor_state(indoc! {"
7735 const a: B = (
7736 c(),
7737 d(
7738 e,
7739 f
7740 )ˇ
7741 );
7742 "});
7743
7744 // Paste it at a line with a lower indent level.
7745 cx.set_state(indoc! {"
7746 ˇ
7747 const a: B = (
7748 c(),
7749 );
7750 "});
7751 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7752 cx.assert_editor_state(indoc! {"
7753 d(
7754 e,
7755 f
7756 )ˇ
7757 const a: B = (
7758 c(),
7759 );
7760 "});
7761
7762 // Cut an indented block, with the leading whitespace.
7763 cx.set_state(indoc! {"
7764 const a: B = (
7765 c(),
7766 « d(
7767 e,
7768 f
7769 )
7770 ˇ»);
7771 "});
7772 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7773 cx.assert_editor_state(indoc! {"
7774 const a: B = (
7775 c(),
7776 ˇ);
7777 "});
7778
7779 // Paste it at the same position.
7780 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7781 cx.assert_editor_state(indoc! {"
7782 const a: B = (
7783 c(),
7784 d(
7785 e,
7786 f
7787 )
7788 ˇ);
7789 "});
7790
7791 // Paste it at a line with a higher indent level.
7792 cx.set_state(indoc! {"
7793 const a: B = (
7794 c(),
7795 d(
7796 e,
7797 fˇ
7798 )
7799 );
7800 "});
7801 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7802 cx.assert_editor_state(indoc! {"
7803 const a: B = (
7804 c(),
7805 d(
7806 e,
7807 f d(
7808 e,
7809 f
7810 )
7811 ˇ
7812 )
7813 );
7814 "});
7815
7816 // Copy an indented block, starting mid-line
7817 cx.set_state(indoc! {"
7818 const a: B = (
7819 c(),
7820 somethin«g(
7821 e,
7822 f
7823 )ˇ»
7824 );
7825 "});
7826 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7827
7828 // Paste it on a line with a lower indent level
7829 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7830 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7831 cx.assert_editor_state(indoc! {"
7832 const a: B = (
7833 c(),
7834 something(
7835 e,
7836 f
7837 )
7838 );
7839 g(
7840 e,
7841 f
7842 )ˇ"});
7843}
7844
7845#[gpui::test]
7846async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7847 init_test(cx, |_| {});
7848
7849 cx.write_to_clipboard(ClipboardItem::new_string(
7850 " d(\n e\n );\n".into(),
7851 ));
7852
7853 let mut cx = EditorTestContext::new(cx).await;
7854 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7855 cx.run_until_parked();
7856
7857 cx.set_state(indoc! {"
7858 fn a() {
7859 b();
7860 if c() {
7861 ˇ
7862 }
7863 }
7864 "});
7865
7866 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7867 cx.assert_editor_state(indoc! {"
7868 fn a() {
7869 b();
7870 if c() {
7871 d(
7872 e
7873 );
7874 ˇ
7875 }
7876 }
7877 "});
7878
7879 cx.set_state(indoc! {"
7880 fn a() {
7881 b();
7882 ˇ
7883 }
7884 "});
7885
7886 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7887 cx.assert_editor_state(indoc! {"
7888 fn a() {
7889 b();
7890 d(
7891 e
7892 );
7893 ˇ
7894 }
7895 "});
7896}
7897
7898#[gpui::test]
7899fn test_select_all(cx: &mut TestAppContext) {
7900 init_test(cx, |_| {});
7901
7902 let editor = cx.add_window(|window, cx| {
7903 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7904 build_editor(buffer, window, cx)
7905 });
7906 _ = editor.update(cx, |editor, window, cx| {
7907 editor.select_all(&SelectAll, window, cx);
7908 assert_eq!(
7909 display_ranges(editor, cx),
7910 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7911 );
7912 });
7913}
7914
7915#[gpui::test]
7916fn test_select_line(cx: &mut TestAppContext) {
7917 init_test(cx, |_| {});
7918
7919 let editor = cx.add_window(|window, cx| {
7920 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7921 build_editor(buffer, window, cx)
7922 });
7923 _ = editor.update(cx, |editor, window, cx| {
7924 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7925 s.select_display_ranges([
7926 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7927 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7928 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7929 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7930 ])
7931 });
7932 editor.select_line(&SelectLine, window, cx);
7933 // Adjacent line selections should NOT merge (only overlapping ones do)
7934 assert_eq!(
7935 display_ranges(editor, cx),
7936 vec![
7937 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(1), 0),
7938 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(2), 0),
7939 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7940 ]
7941 );
7942 });
7943
7944 _ = editor.update(cx, |editor, window, cx| {
7945 editor.select_line(&SelectLine, window, cx);
7946 assert_eq!(
7947 display_ranges(editor, cx),
7948 vec![
7949 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7950 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7951 ]
7952 );
7953 });
7954
7955 _ = editor.update(cx, |editor, window, cx| {
7956 editor.select_line(&SelectLine, window, cx);
7957 // Adjacent but not overlapping, so they stay separate
7958 assert_eq!(
7959 display_ranges(editor, cx),
7960 vec![
7961 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(4), 0),
7962 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7963 ]
7964 );
7965 });
7966}
7967
7968#[gpui::test]
7969async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7970 init_test(cx, |_| {});
7971 let mut cx = EditorTestContext::new(cx).await;
7972
7973 #[track_caller]
7974 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7975 cx.set_state(initial_state);
7976 cx.update_editor(|e, window, cx| {
7977 e.split_selection_into_lines(&Default::default(), window, cx)
7978 });
7979 cx.assert_editor_state(expected_state);
7980 }
7981
7982 // Selection starts and ends at the middle of lines, left-to-right
7983 test(
7984 &mut cx,
7985 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7986 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7987 );
7988 // Same thing, right-to-left
7989 test(
7990 &mut cx,
7991 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7992 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7993 );
7994
7995 // Whole buffer, left-to-right, last line *doesn't* end with newline
7996 test(
7997 &mut cx,
7998 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7999 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
8000 );
8001 // Same thing, right-to-left
8002 test(
8003 &mut cx,
8004 "«aa\nbb\ncc\ndd\nee\nffˇ»",
8005 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
8006 );
8007
8008 // Whole buffer, left-to-right, last line ends with newline
8009 test(
8010 &mut cx,
8011 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
8012 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
8013 );
8014 // Same thing, right-to-left
8015 test(
8016 &mut cx,
8017 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
8018 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
8019 );
8020
8021 // Starts at the end of a line, ends at the start of another
8022 test(
8023 &mut cx,
8024 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
8025 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
8026 );
8027}
8028
8029#[gpui::test]
8030async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
8031 init_test(cx, |_| {});
8032
8033 let editor = cx.add_window(|window, cx| {
8034 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
8035 build_editor(buffer, window, cx)
8036 });
8037
8038 // setup
8039 _ = editor.update(cx, |editor, window, cx| {
8040 editor.fold_creases(
8041 vec![
8042 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
8043 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
8044 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
8045 ],
8046 true,
8047 window,
8048 cx,
8049 );
8050 assert_eq!(
8051 editor.display_text(cx),
8052 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
8053 );
8054 });
8055
8056 _ = editor.update(cx, |editor, window, cx| {
8057 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8058 s.select_display_ranges([
8059 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8060 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
8061 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
8062 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
8063 ])
8064 });
8065 editor.split_selection_into_lines(&Default::default(), window, cx);
8066 assert_eq!(
8067 editor.display_text(cx),
8068 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
8069 );
8070 });
8071 EditorTestContext::for_editor(editor, cx)
8072 .await
8073 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
8074
8075 _ = editor.update(cx, |editor, window, cx| {
8076 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8077 s.select_display_ranges([
8078 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
8079 ])
8080 });
8081 editor.split_selection_into_lines(&Default::default(), window, cx);
8082 assert_eq!(
8083 editor.display_text(cx),
8084 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
8085 );
8086 assert_eq!(
8087 display_ranges(editor, cx),
8088 [
8089 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
8090 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
8091 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
8092 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
8093 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
8094 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
8095 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
8096 ]
8097 );
8098 });
8099 EditorTestContext::for_editor(editor, cx)
8100 .await
8101 .assert_editor_state(
8102 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
8103 );
8104}
8105
8106#[gpui::test]
8107async fn test_add_selection_above_below(cx: &mut TestAppContext) {
8108 init_test(cx, |_| {});
8109
8110 let mut cx = EditorTestContext::new(cx).await;
8111
8112 cx.set_state(indoc!(
8113 r#"abc
8114 defˇghi
8115
8116 jk
8117 nlmo
8118 "#
8119 ));
8120
8121 cx.update_editor(|editor, window, cx| {
8122 editor.add_selection_above(&Default::default(), window, cx);
8123 });
8124
8125 cx.assert_editor_state(indoc!(
8126 r#"abcˇ
8127 defˇghi
8128
8129 jk
8130 nlmo
8131 "#
8132 ));
8133
8134 cx.update_editor(|editor, window, cx| {
8135 editor.add_selection_above(&Default::default(), window, cx);
8136 });
8137
8138 cx.assert_editor_state(indoc!(
8139 r#"abcˇ
8140 defˇghi
8141
8142 jk
8143 nlmo
8144 "#
8145 ));
8146
8147 cx.update_editor(|editor, window, cx| {
8148 editor.add_selection_below(&Default::default(), window, cx);
8149 });
8150
8151 cx.assert_editor_state(indoc!(
8152 r#"abc
8153 defˇghi
8154
8155 jk
8156 nlmo
8157 "#
8158 ));
8159
8160 cx.update_editor(|editor, window, cx| {
8161 editor.undo_selection(&Default::default(), window, cx);
8162 });
8163
8164 cx.assert_editor_state(indoc!(
8165 r#"abcˇ
8166 defˇghi
8167
8168 jk
8169 nlmo
8170 "#
8171 ));
8172
8173 cx.update_editor(|editor, window, cx| {
8174 editor.redo_selection(&Default::default(), window, cx);
8175 });
8176
8177 cx.assert_editor_state(indoc!(
8178 r#"abc
8179 defˇghi
8180
8181 jk
8182 nlmo
8183 "#
8184 ));
8185
8186 cx.update_editor(|editor, window, cx| {
8187 editor.add_selection_below(&Default::default(), window, cx);
8188 });
8189
8190 cx.assert_editor_state(indoc!(
8191 r#"abc
8192 defˇghi
8193 ˇ
8194 jk
8195 nlmo
8196 "#
8197 ));
8198
8199 cx.update_editor(|editor, window, cx| {
8200 editor.add_selection_below(&Default::default(), window, cx);
8201 });
8202
8203 cx.assert_editor_state(indoc!(
8204 r#"abc
8205 defˇghi
8206 ˇ
8207 jkˇ
8208 nlmo
8209 "#
8210 ));
8211
8212 cx.update_editor(|editor, window, cx| {
8213 editor.add_selection_below(&Default::default(), window, cx);
8214 });
8215
8216 cx.assert_editor_state(indoc!(
8217 r#"abc
8218 defˇghi
8219 ˇ
8220 jkˇ
8221 nlmˇo
8222 "#
8223 ));
8224
8225 cx.update_editor(|editor, window, cx| {
8226 editor.add_selection_below(&Default::default(), window, cx);
8227 });
8228
8229 cx.assert_editor_state(indoc!(
8230 r#"abc
8231 defˇghi
8232 ˇ
8233 jkˇ
8234 nlmˇo
8235 ˇ"#
8236 ));
8237
8238 // change selections
8239 cx.set_state(indoc!(
8240 r#"abc
8241 def«ˇg»hi
8242
8243 jk
8244 nlmo
8245 "#
8246 ));
8247
8248 cx.update_editor(|editor, window, cx| {
8249 editor.add_selection_below(&Default::default(), window, cx);
8250 });
8251
8252 cx.assert_editor_state(indoc!(
8253 r#"abc
8254 def«ˇg»hi
8255
8256 jk
8257 nlm«ˇo»
8258 "#
8259 ));
8260
8261 cx.update_editor(|editor, window, cx| {
8262 editor.add_selection_below(&Default::default(), window, cx);
8263 });
8264
8265 cx.assert_editor_state(indoc!(
8266 r#"abc
8267 def«ˇg»hi
8268
8269 jk
8270 nlm«ˇo»
8271 "#
8272 ));
8273
8274 cx.update_editor(|editor, window, cx| {
8275 editor.add_selection_above(&Default::default(), window, cx);
8276 });
8277
8278 cx.assert_editor_state(indoc!(
8279 r#"abc
8280 def«ˇg»hi
8281
8282 jk
8283 nlmo
8284 "#
8285 ));
8286
8287 cx.update_editor(|editor, window, cx| {
8288 editor.add_selection_above(&Default::default(), window, cx);
8289 });
8290
8291 cx.assert_editor_state(indoc!(
8292 r#"abc
8293 def«ˇg»hi
8294
8295 jk
8296 nlmo
8297 "#
8298 ));
8299
8300 // Change selections again
8301 cx.set_state(indoc!(
8302 r#"a«bc
8303 defgˇ»hi
8304
8305 jk
8306 nlmo
8307 "#
8308 ));
8309
8310 cx.update_editor(|editor, window, cx| {
8311 editor.add_selection_below(&Default::default(), window, cx);
8312 });
8313
8314 cx.assert_editor_state(indoc!(
8315 r#"a«bcˇ»
8316 d«efgˇ»hi
8317
8318 j«kˇ»
8319 nlmo
8320 "#
8321 ));
8322
8323 cx.update_editor(|editor, window, cx| {
8324 editor.add_selection_below(&Default::default(), window, cx);
8325 });
8326 cx.assert_editor_state(indoc!(
8327 r#"a«bcˇ»
8328 d«efgˇ»hi
8329
8330 j«kˇ»
8331 n«lmoˇ»
8332 "#
8333 ));
8334 cx.update_editor(|editor, window, cx| {
8335 editor.add_selection_above(&Default::default(), window, cx);
8336 });
8337
8338 cx.assert_editor_state(indoc!(
8339 r#"a«bcˇ»
8340 d«efgˇ»hi
8341
8342 j«kˇ»
8343 nlmo
8344 "#
8345 ));
8346
8347 // Change selections again
8348 cx.set_state(indoc!(
8349 r#"abc
8350 d«ˇefghi
8351
8352 jk
8353 nlm»o
8354 "#
8355 ));
8356
8357 cx.update_editor(|editor, window, cx| {
8358 editor.add_selection_above(&Default::default(), window, cx);
8359 });
8360
8361 cx.assert_editor_state(indoc!(
8362 r#"a«ˇbc»
8363 d«ˇef»ghi
8364
8365 j«ˇk»
8366 n«ˇlm»o
8367 "#
8368 ));
8369
8370 cx.update_editor(|editor, window, cx| {
8371 editor.add_selection_below(&Default::default(), window, cx);
8372 });
8373
8374 cx.assert_editor_state(indoc!(
8375 r#"abc
8376 d«ˇef»ghi
8377
8378 j«ˇk»
8379 n«ˇlm»o
8380 "#
8381 ));
8382
8383 // Assert that the oldest selection's goal column is used when adding more
8384 // selections, not the most recently added selection's actual column.
8385 cx.set_state(indoc! {"
8386 foo bar bazˇ
8387 foo
8388 foo bar
8389 "});
8390
8391 cx.update_editor(|editor, window, cx| {
8392 editor.add_selection_below(
8393 &AddSelectionBelow {
8394 skip_soft_wrap: true,
8395 },
8396 window,
8397 cx,
8398 );
8399 });
8400
8401 cx.assert_editor_state(indoc! {"
8402 foo bar bazˇ
8403 fooˇ
8404 foo bar
8405 "});
8406
8407 cx.update_editor(|editor, window, cx| {
8408 editor.add_selection_below(
8409 &AddSelectionBelow {
8410 skip_soft_wrap: true,
8411 },
8412 window,
8413 cx,
8414 );
8415 });
8416
8417 cx.assert_editor_state(indoc! {"
8418 foo bar bazˇ
8419 fooˇ
8420 foo barˇ
8421 "});
8422
8423 cx.set_state(indoc! {"
8424 foo bar baz
8425 foo
8426 foo barˇ
8427 "});
8428
8429 cx.update_editor(|editor, window, cx| {
8430 editor.add_selection_above(
8431 &AddSelectionAbove {
8432 skip_soft_wrap: true,
8433 },
8434 window,
8435 cx,
8436 );
8437 });
8438
8439 cx.assert_editor_state(indoc! {"
8440 foo bar baz
8441 fooˇ
8442 foo barˇ
8443 "});
8444
8445 cx.update_editor(|editor, window, cx| {
8446 editor.add_selection_above(
8447 &AddSelectionAbove {
8448 skip_soft_wrap: true,
8449 },
8450 window,
8451 cx,
8452 );
8453 });
8454
8455 cx.assert_editor_state(indoc! {"
8456 foo barˇ baz
8457 fooˇ
8458 foo barˇ
8459 "});
8460}
8461
8462#[gpui::test]
8463async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8464 init_test(cx, |_| {});
8465 let mut cx = EditorTestContext::new(cx).await;
8466
8467 cx.set_state(indoc!(
8468 r#"line onˇe
8469 liˇne two
8470 line three
8471 line four"#
8472 ));
8473
8474 cx.update_editor(|editor, window, cx| {
8475 editor.add_selection_below(&Default::default(), window, cx);
8476 });
8477
8478 // test multiple cursors expand in the same direction
8479 cx.assert_editor_state(indoc!(
8480 r#"line onˇe
8481 liˇne twˇo
8482 liˇne three
8483 line four"#
8484 ));
8485
8486 cx.update_editor(|editor, window, cx| {
8487 editor.add_selection_below(&Default::default(), window, cx);
8488 });
8489
8490 cx.update_editor(|editor, window, cx| {
8491 editor.add_selection_below(&Default::default(), window, cx);
8492 });
8493
8494 // test multiple cursors expand below overflow
8495 cx.assert_editor_state(indoc!(
8496 r#"line onˇe
8497 liˇne twˇo
8498 liˇne thˇree
8499 liˇne foˇur"#
8500 ));
8501
8502 cx.update_editor(|editor, window, cx| {
8503 editor.add_selection_above(&Default::default(), window, cx);
8504 });
8505
8506 // test multiple cursors retrieves back correctly
8507 cx.assert_editor_state(indoc!(
8508 r#"line onˇe
8509 liˇne twˇo
8510 liˇne thˇree
8511 line four"#
8512 ));
8513
8514 cx.update_editor(|editor, window, cx| {
8515 editor.add_selection_above(&Default::default(), window, cx);
8516 });
8517
8518 cx.update_editor(|editor, window, cx| {
8519 editor.add_selection_above(&Default::default(), window, cx);
8520 });
8521
8522 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8523 cx.assert_editor_state(indoc!(
8524 r#"liˇne onˇe
8525 liˇne two
8526 line three
8527 line four"#
8528 ));
8529
8530 cx.update_editor(|editor, window, cx| {
8531 editor.undo_selection(&Default::default(), window, cx);
8532 });
8533
8534 // test undo
8535 cx.assert_editor_state(indoc!(
8536 r#"line onˇe
8537 liˇne twˇo
8538 line three
8539 line four"#
8540 ));
8541
8542 cx.update_editor(|editor, window, cx| {
8543 editor.redo_selection(&Default::default(), window, cx);
8544 });
8545
8546 // test redo
8547 cx.assert_editor_state(indoc!(
8548 r#"liˇne onˇe
8549 liˇne two
8550 line three
8551 line four"#
8552 ));
8553
8554 cx.set_state(indoc!(
8555 r#"abcd
8556 ef«ghˇ»
8557 ijkl
8558 «mˇ»nop"#
8559 ));
8560
8561 cx.update_editor(|editor, window, cx| {
8562 editor.add_selection_above(&Default::default(), window, cx);
8563 });
8564
8565 // test multiple selections expand in the same direction
8566 cx.assert_editor_state(indoc!(
8567 r#"ab«cdˇ»
8568 ef«ghˇ»
8569 «iˇ»jkl
8570 «mˇ»nop"#
8571 ));
8572
8573 cx.update_editor(|editor, window, cx| {
8574 editor.add_selection_above(&Default::default(), window, cx);
8575 });
8576
8577 // test multiple selection upward overflow
8578 cx.assert_editor_state(indoc!(
8579 r#"ab«cdˇ»
8580 «eˇ»f«ghˇ»
8581 «iˇ»jkl
8582 «mˇ»nop"#
8583 ));
8584
8585 cx.update_editor(|editor, window, cx| {
8586 editor.add_selection_below(&Default::default(), window, cx);
8587 });
8588
8589 // test multiple selection retrieves back correctly
8590 cx.assert_editor_state(indoc!(
8591 r#"abcd
8592 ef«ghˇ»
8593 «iˇ»jkl
8594 «mˇ»nop"#
8595 ));
8596
8597 cx.update_editor(|editor, window, cx| {
8598 editor.add_selection_below(&Default::default(), window, cx);
8599 });
8600
8601 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8602 cx.assert_editor_state(indoc!(
8603 r#"abcd
8604 ef«ghˇ»
8605 ij«klˇ»
8606 «mˇ»nop"#
8607 ));
8608
8609 cx.update_editor(|editor, window, cx| {
8610 editor.undo_selection(&Default::default(), window, cx);
8611 });
8612
8613 // test undo
8614 cx.assert_editor_state(indoc!(
8615 r#"abcd
8616 ef«ghˇ»
8617 «iˇ»jkl
8618 «mˇ»nop"#
8619 ));
8620
8621 cx.update_editor(|editor, window, cx| {
8622 editor.redo_selection(&Default::default(), window, cx);
8623 });
8624
8625 // test redo
8626 cx.assert_editor_state(indoc!(
8627 r#"abcd
8628 ef«ghˇ»
8629 ij«klˇ»
8630 «mˇ»nop"#
8631 ));
8632}
8633
8634#[gpui::test]
8635async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8636 init_test(cx, |_| {});
8637 let mut cx = EditorTestContext::new(cx).await;
8638
8639 cx.set_state(indoc!(
8640 r#"line onˇe
8641 liˇne two
8642 line three
8643 line four"#
8644 ));
8645
8646 cx.update_editor(|editor, window, cx| {
8647 editor.add_selection_below(&Default::default(), window, cx);
8648 editor.add_selection_below(&Default::default(), window, cx);
8649 editor.add_selection_below(&Default::default(), window, cx);
8650 });
8651
8652 // initial state with two multi cursor groups
8653 cx.assert_editor_state(indoc!(
8654 r#"line onˇe
8655 liˇne twˇo
8656 liˇne thˇree
8657 liˇne foˇur"#
8658 ));
8659
8660 // add single cursor in middle - simulate opt click
8661 cx.update_editor(|editor, window, cx| {
8662 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8663 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8664 editor.end_selection(window, cx);
8665 });
8666
8667 cx.assert_editor_state(indoc!(
8668 r#"line onˇe
8669 liˇne twˇo
8670 liˇneˇ thˇree
8671 liˇne foˇur"#
8672 ));
8673
8674 cx.update_editor(|editor, window, cx| {
8675 editor.add_selection_above(&Default::default(), window, cx);
8676 });
8677
8678 // test new added selection expands above and existing selection shrinks
8679 cx.assert_editor_state(indoc!(
8680 r#"line onˇe
8681 liˇneˇ twˇo
8682 liˇneˇ thˇree
8683 line four"#
8684 ));
8685
8686 cx.update_editor(|editor, window, cx| {
8687 editor.add_selection_above(&Default::default(), window, cx);
8688 });
8689
8690 // test new added selection expands above and existing selection shrinks
8691 cx.assert_editor_state(indoc!(
8692 r#"lineˇ onˇe
8693 liˇneˇ twˇo
8694 lineˇ three
8695 line four"#
8696 ));
8697
8698 // intial state with two selection groups
8699 cx.set_state(indoc!(
8700 r#"abcd
8701 ef«ghˇ»
8702 ijkl
8703 «mˇ»nop"#
8704 ));
8705
8706 cx.update_editor(|editor, window, cx| {
8707 editor.add_selection_above(&Default::default(), window, cx);
8708 editor.add_selection_above(&Default::default(), window, cx);
8709 });
8710
8711 cx.assert_editor_state(indoc!(
8712 r#"ab«cdˇ»
8713 «eˇ»f«ghˇ»
8714 «iˇ»jkl
8715 «mˇ»nop"#
8716 ));
8717
8718 // add single selection in middle - simulate opt drag
8719 cx.update_editor(|editor, window, cx| {
8720 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8721 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8722 editor.update_selection(
8723 DisplayPoint::new(DisplayRow(2), 4),
8724 0,
8725 gpui::Point::<f32>::default(),
8726 window,
8727 cx,
8728 );
8729 editor.end_selection(window, cx);
8730 });
8731
8732 cx.assert_editor_state(indoc!(
8733 r#"ab«cdˇ»
8734 «eˇ»f«ghˇ»
8735 «iˇ»jk«lˇ»
8736 «mˇ»nop"#
8737 ));
8738
8739 cx.update_editor(|editor, window, cx| {
8740 editor.add_selection_below(&Default::default(), window, cx);
8741 });
8742
8743 // test new added selection expands below, others shrinks from above
8744 cx.assert_editor_state(indoc!(
8745 r#"abcd
8746 ef«ghˇ»
8747 «iˇ»jk«lˇ»
8748 «mˇ»no«pˇ»"#
8749 ));
8750}
8751
8752#[gpui::test]
8753async fn test_select_next(cx: &mut TestAppContext) {
8754 init_test(cx, |_| {});
8755 let mut cx = EditorTestContext::new(cx).await;
8756
8757 // Enable case sensitive search.
8758 update_test_editor_settings(&mut cx, |settings| {
8759 let mut search_settings = SearchSettingsContent::default();
8760 search_settings.case_sensitive = Some(true);
8761 settings.search = Some(search_settings);
8762 });
8763
8764 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8765
8766 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8767 .unwrap();
8768 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8769
8770 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8771 .unwrap();
8772 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8773
8774 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8775 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8776
8777 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8778 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8779
8780 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8781 .unwrap();
8782 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8783
8784 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8785 .unwrap();
8786 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8787
8788 // Test selection direction should be preserved
8789 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8790
8791 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8792 .unwrap();
8793 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8794
8795 // Test case sensitivity
8796 cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo");
8797 cx.update_editor(|e, window, cx| {
8798 e.select_next(&SelectNext::default(), window, cx).unwrap();
8799 });
8800 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
8801
8802 // Disable case sensitive search.
8803 update_test_editor_settings(&mut cx, |settings| {
8804 let mut search_settings = SearchSettingsContent::default();
8805 search_settings.case_sensitive = Some(false);
8806 settings.search = Some(search_settings);
8807 });
8808
8809 cx.set_state("«ˇfoo»\nFOO\nFoo");
8810 cx.update_editor(|e, window, cx| {
8811 e.select_next(&SelectNext::default(), window, cx).unwrap();
8812 e.select_next(&SelectNext::default(), window, cx).unwrap();
8813 });
8814 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
8815}
8816
8817#[gpui::test]
8818async fn test_select_all_matches(cx: &mut TestAppContext) {
8819 init_test(cx, |_| {});
8820 let mut cx = EditorTestContext::new(cx).await;
8821
8822 // Enable case sensitive search.
8823 update_test_editor_settings(&mut cx, |settings| {
8824 let mut search_settings = SearchSettingsContent::default();
8825 search_settings.case_sensitive = Some(true);
8826 settings.search = Some(search_settings);
8827 });
8828
8829 // Test caret-only selections
8830 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8831 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8832 .unwrap();
8833 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8834
8835 // Test left-to-right selections
8836 cx.set_state("abc\n«abcˇ»\nabc");
8837 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8838 .unwrap();
8839 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8840
8841 // Test right-to-left selections
8842 cx.set_state("abc\n«ˇabc»\nabc");
8843 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8844 .unwrap();
8845 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8846
8847 // Test selecting whitespace with caret selection
8848 cx.set_state("abc\nˇ abc\nabc");
8849 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8850 .unwrap();
8851 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8852
8853 // Test selecting whitespace with left-to-right selection
8854 cx.set_state("abc\n«ˇ »abc\nabc");
8855 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8856 .unwrap();
8857 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8858
8859 // Test no matches with right-to-left selection
8860 cx.set_state("abc\n« ˇ»abc\nabc");
8861 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8862 .unwrap();
8863 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8864
8865 // Test with a single word and clip_at_line_ends=true (#29823)
8866 cx.set_state("aˇbc");
8867 cx.update_editor(|e, window, cx| {
8868 e.set_clip_at_line_ends(true, cx);
8869 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8870 e.set_clip_at_line_ends(false, cx);
8871 });
8872 cx.assert_editor_state("«abcˇ»");
8873
8874 // Test case sensitivity
8875 cx.set_state("fˇoo\nFOO\nFoo");
8876 cx.update_editor(|e, window, cx| {
8877 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8878 });
8879 cx.assert_editor_state("«fooˇ»\nFOO\nFoo");
8880
8881 // Disable case sensitive search.
8882 update_test_editor_settings(&mut cx, |settings| {
8883 let mut search_settings = SearchSettingsContent::default();
8884 search_settings.case_sensitive = Some(false);
8885 settings.search = Some(search_settings);
8886 });
8887
8888 cx.set_state("fˇoo\nFOO\nFoo");
8889 cx.update_editor(|e, window, cx| {
8890 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8891 });
8892 cx.assert_editor_state("«fooˇ»\n«FOOˇ»\n«Fooˇ»");
8893}
8894
8895#[gpui::test]
8896async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8897 init_test(cx, |_| {});
8898
8899 let mut cx = EditorTestContext::new(cx).await;
8900
8901 let large_body_1 = "\nd".repeat(200);
8902 let large_body_2 = "\ne".repeat(200);
8903
8904 cx.set_state(&format!(
8905 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8906 ));
8907 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8908 let scroll_position = editor.scroll_position(cx);
8909 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8910 scroll_position
8911 });
8912
8913 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8914 .unwrap();
8915 cx.assert_editor_state(&format!(
8916 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8917 ));
8918 let scroll_position_after_selection =
8919 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8920 assert_eq!(
8921 initial_scroll_position, scroll_position_after_selection,
8922 "Scroll position should not change after selecting all matches"
8923 );
8924}
8925
8926#[gpui::test]
8927async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8928 init_test(cx, |_| {});
8929
8930 let mut cx = EditorLspTestContext::new_rust(
8931 lsp::ServerCapabilities {
8932 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8933 ..Default::default()
8934 },
8935 cx,
8936 )
8937 .await;
8938
8939 cx.set_state(indoc! {"
8940 line 1
8941 line 2
8942 linˇe 3
8943 line 4
8944 line 5
8945 "});
8946
8947 // Make an edit
8948 cx.update_editor(|editor, window, cx| {
8949 editor.handle_input("X", window, cx);
8950 });
8951
8952 // Move cursor to a different position
8953 cx.update_editor(|editor, window, cx| {
8954 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8955 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8956 });
8957 });
8958
8959 cx.assert_editor_state(indoc! {"
8960 line 1
8961 line 2
8962 linXe 3
8963 line 4
8964 liˇne 5
8965 "});
8966
8967 cx.lsp
8968 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8969 Ok(Some(vec![lsp::TextEdit::new(
8970 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8971 "PREFIX ".to_string(),
8972 )]))
8973 });
8974
8975 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8976 .unwrap()
8977 .await
8978 .unwrap();
8979
8980 cx.assert_editor_state(indoc! {"
8981 PREFIX line 1
8982 line 2
8983 linXe 3
8984 line 4
8985 liˇne 5
8986 "});
8987
8988 // Undo formatting
8989 cx.update_editor(|editor, window, cx| {
8990 editor.undo(&Default::default(), window, cx);
8991 });
8992
8993 // Verify cursor moved back to position after edit
8994 cx.assert_editor_state(indoc! {"
8995 line 1
8996 line 2
8997 linXˇe 3
8998 line 4
8999 line 5
9000 "});
9001}
9002
9003#[gpui::test]
9004async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
9005 init_test(cx, |_| {});
9006
9007 let mut cx = EditorTestContext::new(cx).await;
9008
9009 let provider = cx.new(|_| FakeEditPredictionDelegate::default());
9010 cx.update_editor(|editor, window, cx| {
9011 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
9012 });
9013
9014 cx.set_state(indoc! {"
9015 line 1
9016 line 2
9017 linˇe 3
9018 line 4
9019 line 5
9020 line 6
9021 line 7
9022 line 8
9023 line 9
9024 line 10
9025 "});
9026
9027 let snapshot = cx.buffer_snapshot();
9028 let edit_position = snapshot.anchor_after(Point::new(2, 4));
9029
9030 cx.update(|_, cx| {
9031 provider.update(cx, |provider, _| {
9032 provider.set_edit_prediction(Some(edit_prediction_types::EditPrediction::Local {
9033 id: None,
9034 edits: vec![(edit_position..edit_position, "X".into())],
9035 cursor_position: None,
9036 edit_preview: None,
9037 }))
9038 })
9039 });
9040
9041 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
9042 cx.update_editor(|editor, window, cx| {
9043 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
9044 });
9045
9046 cx.assert_editor_state(indoc! {"
9047 line 1
9048 line 2
9049 lineXˇ 3
9050 line 4
9051 line 5
9052 line 6
9053 line 7
9054 line 8
9055 line 9
9056 line 10
9057 "});
9058
9059 cx.update_editor(|editor, window, cx| {
9060 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9061 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
9062 });
9063 });
9064
9065 cx.assert_editor_state(indoc! {"
9066 line 1
9067 line 2
9068 lineX 3
9069 line 4
9070 line 5
9071 line 6
9072 line 7
9073 line 8
9074 line 9
9075 liˇne 10
9076 "});
9077
9078 cx.update_editor(|editor, window, cx| {
9079 editor.undo(&Default::default(), window, cx);
9080 });
9081
9082 cx.assert_editor_state(indoc! {"
9083 line 1
9084 line 2
9085 lineˇ 3
9086 line 4
9087 line 5
9088 line 6
9089 line 7
9090 line 8
9091 line 9
9092 line 10
9093 "});
9094}
9095
9096#[gpui::test]
9097async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
9098 init_test(cx, |_| {});
9099
9100 let mut cx = EditorTestContext::new(cx).await;
9101 cx.set_state(
9102 r#"let foo = 2;
9103lˇet foo = 2;
9104let fooˇ = 2;
9105let foo = 2;
9106let foo = ˇ2;"#,
9107 );
9108
9109 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9110 .unwrap();
9111 cx.assert_editor_state(
9112 r#"let foo = 2;
9113«letˇ» foo = 2;
9114let «fooˇ» = 2;
9115let foo = 2;
9116let foo = «2ˇ»;"#,
9117 );
9118
9119 // noop for multiple selections with different contents
9120 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9121 .unwrap();
9122 cx.assert_editor_state(
9123 r#"let foo = 2;
9124«letˇ» foo = 2;
9125let «fooˇ» = 2;
9126let foo = 2;
9127let foo = «2ˇ»;"#,
9128 );
9129
9130 // Test last selection direction should be preserved
9131 cx.set_state(
9132 r#"let foo = 2;
9133let foo = 2;
9134let «fooˇ» = 2;
9135let «ˇfoo» = 2;
9136let foo = 2;"#,
9137 );
9138
9139 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
9140 .unwrap();
9141 cx.assert_editor_state(
9142 r#"let foo = 2;
9143let foo = 2;
9144let «fooˇ» = 2;
9145let «ˇfoo» = 2;
9146let «ˇfoo» = 2;"#,
9147 );
9148}
9149
9150#[gpui::test]
9151async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
9152 init_test(cx, |_| {});
9153
9154 let mut cx =
9155 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
9156
9157 cx.assert_editor_state(indoc! {"
9158 ˇbbb
9159 ccc
9160
9161 bbb
9162 ccc
9163 "});
9164 cx.dispatch_action(SelectPrevious::default());
9165 cx.assert_editor_state(indoc! {"
9166 «bbbˇ»
9167 ccc
9168
9169 bbb
9170 ccc
9171 "});
9172 cx.dispatch_action(SelectPrevious::default());
9173 cx.assert_editor_state(indoc! {"
9174 «bbbˇ»
9175 ccc
9176
9177 «bbbˇ»
9178 ccc
9179 "});
9180}
9181
9182#[gpui::test]
9183async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
9184 init_test(cx, |_| {});
9185
9186 let mut cx = EditorTestContext::new(cx).await;
9187 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
9188
9189 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9190 .unwrap();
9191 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
9192
9193 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9194 .unwrap();
9195 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
9196
9197 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
9198 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
9199
9200 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
9201 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
9202
9203 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9204 .unwrap();
9205 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
9206
9207 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9208 .unwrap();
9209 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
9210}
9211
9212#[gpui::test]
9213async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
9214 init_test(cx, |_| {});
9215
9216 let mut cx = EditorTestContext::new(cx).await;
9217 cx.set_state("aˇ");
9218
9219 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9220 .unwrap();
9221 cx.assert_editor_state("«aˇ»");
9222 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9223 .unwrap();
9224 cx.assert_editor_state("«aˇ»");
9225}
9226
9227#[gpui::test]
9228async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
9229 init_test(cx, |_| {});
9230
9231 let mut cx = EditorTestContext::new(cx).await;
9232 cx.set_state(
9233 r#"let foo = 2;
9234lˇet foo = 2;
9235let fooˇ = 2;
9236let foo = 2;
9237let foo = ˇ2;"#,
9238 );
9239
9240 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9241 .unwrap();
9242 cx.assert_editor_state(
9243 r#"let foo = 2;
9244«letˇ» foo = 2;
9245let «fooˇ» = 2;
9246let foo = 2;
9247let foo = «2ˇ»;"#,
9248 );
9249
9250 // noop for multiple selections with different contents
9251 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9252 .unwrap();
9253 cx.assert_editor_state(
9254 r#"let foo = 2;
9255«letˇ» foo = 2;
9256let «fooˇ» = 2;
9257let foo = 2;
9258let foo = «2ˇ»;"#,
9259 );
9260}
9261
9262#[gpui::test]
9263async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
9264 init_test(cx, |_| {});
9265 let mut cx = EditorTestContext::new(cx).await;
9266
9267 // Enable case sensitive search.
9268 update_test_editor_settings(&mut cx, |settings| {
9269 let mut search_settings = SearchSettingsContent::default();
9270 search_settings.case_sensitive = Some(true);
9271 settings.search = Some(search_settings);
9272 });
9273
9274 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
9275
9276 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9277 .unwrap();
9278 // selection direction is preserved
9279 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
9280
9281 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9282 .unwrap();
9283 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
9284
9285 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
9286 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
9287
9288 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
9289 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
9290
9291 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9292 .unwrap();
9293 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
9294
9295 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
9296 .unwrap();
9297 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
9298
9299 // Test case sensitivity
9300 cx.set_state("foo\nFOO\nFoo\n«ˇfoo»");
9301 cx.update_editor(|e, window, cx| {
9302 e.select_previous(&SelectPrevious::default(), window, cx)
9303 .unwrap();
9304 e.select_previous(&SelectPrevious::default(), window, cx)
9305 .unwrap();
9306 });
9307 cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»");
9308
9309 // Disable case sensitive search.
9310 update_test_editor_settings(&mut cx, |settings| {
9311 let mut search_settings = SearchSettingsContent::default();
9312 search_settings.case_sensitive = Some(false);
9313 settings.search = Some(search_settings);
9314 });
9315
9316 cx.set_state("foo\nFOO\n«ˇFoo»");
9317 cx.update_editor(|e, window, cx| {
9318 e.select_previous(&SelectPrevious::default(), window, cx)
9319 .unwrap();
9320 e.select_previous(&SelectPrevious::default(), window, cx)
9321 .unwrap();
9322 });
9323 cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\n«ˇFoo»");
9324}
9325
9326#[gpui::test]
9327async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
9328 init_test(cx, |_| {});
9329
9330 let language = Arc::new(Language::new(
9331 LanguageConfig::default(),
9332 Some(tree_sitter_rust::LANGUAGE.into()),
9333 ));
9334
9335 let text = r#"
9336 use mod1::mod2::{mod3, mod4};
9337
9338 fn fn_1(param1: bool, param2: &str) {
9339 let var1 = "text";
9340 }
9341 "#
9342 .unindent();
9343
9344 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9345 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9346 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9347
9348 editor
9349 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9350 .await;
9351
9352 editor.update_in(cx, |editor, window, cx| {
9353 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9354 s.select_display_ranges([
9355 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
9356 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
9357 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
9358 ]);
9359 });
9360 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9361 });
9362 editor.update(cx, |editor, cx| {
9363 assert_text_with_selections(
9364 editor,
9365 indoc! {r#"
9366 use mod1::mod2::{mod3, «mod4ˇ»};
9367
9368 fn fn_1«ˇ(param1: bool, param2: &str)» {
9369 let var1 = "«ˇtext»";
9370 }
9371 "#},
9372 cx,
9373 );
9374 });
9375
9376 editor.update_in(cx, |editor, window, cx| {
9377 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9378 });
9379 editor.update(cx, |editor, cx| {
9380 assert_text_with_selections(
9381 editor,
9382 indoc! {r#"
9383 use mod1::mod2::«{mod3, mod4}ˇ»;
9384
9385 «ˇfn fn_1(param1: bool, param2: &str) {
9386 let var1 = "text";
9387 }»
9388 "#},
9389 cx,
9390 );
9391 });
9392
9393 editor.update_in(cx, |editor, window, cx| {
9394 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9395 });
9396 assert_eq!(
9397 editor.update(cx, |editor, cx| editor
9398 .selections
9399 .display_ranges(&editor.display_snapshot(cx))),
9400 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9401 );
9402
9403 // Trying to expand the selected syntax node one more time has no effect.
9404 editor.update_in(cx, |editor, window, cx| {
9405 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9406 });
9407 assert_eq!(
9408 editor.update(cx, |editor, cx| editor
9409 .selections
9410 .display_ranges(&editor.display_snapshot(cx))),
9411 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
9412 );
9413
9414 editor.update_in(cx, |editor, window, cx| {
9415 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9416 });
9417 editor.update(cx, |editor, cx| {
9418 assert_text_with_selections(
9419 editor,
9420 indoc! {r#"
9421 use mod1::mod2::«{mod3, mod4}ˇ»;
9422
9423 «ˇfn fn_1(param1: bool, param2: &str) {
9424 let var1 = "text";
9425 }»
9426 "#},
9427 cx,
9428 );
9429 });
9430
9431 editor.update_in(cx, |editor, window, cx| {
9432 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9433 });
9434 editor.update(cx, |editor, cx| {
9435 assert_text_with_selections(
9436 editor,
9437 indoc! {r#"
9438 use mod1::mod2::{mod3, «mod4ˇ»};
9439
9440 fn fn_1«ˇ(param1: bool, param2: &str)» {
9441 let var1 = "«ˇtext»";
9442 }
9443 "#},
9444 cx,
9445 );
9446 });
9447
9448 editor.update_in(cx, |editor, window, cx| {
9449 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9450 });
9451 editor.update(cx, |editor, cx| {
9452 assert_text_with_selections(
9453 editor,
9454 indoc! {r#"
9455 use mod1::mod2::{mod3, moˇd4};
9456
9457 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9458 let var1 = "teˇxt";
9459 }
9460 "#},
9461 cx,
9462 );
9463 });
9464
9465 // Trying to shrink the selected syntax node one more time has no effect.
9466 editor.update_in(cx, |editor, window, cx| {
9467 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
9468 });
9469 editor.update_in(cx, |editor, _, cx| {
9470 assert_text_with_selections(
9471 editor,
9472 indoc! {r#"
9473 use mod1::mod2::{mod3, moˇd4};
9474
9475 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
9476 let var1 = "teˇxt";
9477 }
9478 "#},
9479 cx,
9480 );
9481 });
9482
9483 // Ensure that we keep expanding the selection if the larger selection starts or ends within
9484 // a fold.
9485 editor.update_in(cx, |editor, window, cx| {
9486 editor.fold_creases(
9487 vec![
9488 Crease::simple(
9489 Point::new(0, 21)..Point::new(0, 24),
9490 FoldPlaceholder::test(),
9491 ),
9492 Crease::simple(
9493 Point::new(3, 20)..Point::new(3, 22),
9494 FoldPlaceholder::test(),
9495 ),
9496 ],
9497 true,
9498 window,
9499 cx,
9500 );
9501 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9502 });
9503 editor.update(cx, |editor, cx| {
9504 assert_text_with_selections(
9505 editor,
9506 indoc! {r#"
9507 use mod1::mod2::«{mod3, mod4}ˇ»;
9508
9509 fn fn_1«ˇ(param1: bool, param2: &str)» {
9510 let var1 = "«ˇtext»";
9511 }
9512 "#},
9513 cx,
9514 );
9515 });
9516}
9517
9518#[gpui::test]
9519async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
9520 init_test(cx, |_| {});
9521
9522 let language = Arc::new(Language::new(
9523 LanguageConfig::default(),
9524 Some(tree_sitter_rust::LANGUAGE.into()),
9525 ));
9526
9527 let text = "let a = 2;";
9528
9529 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9530 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9531 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9532
9533 editor
9534 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9535 .await;
9536
9537 // Test case 1: Cursor at end of word
9538 editor.update_in(cx, |editor, window, cx| {
9539 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9540 s.select_display_ranges([
9541 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9542 ]);
9543 });
9544 });
9545 editor.update(cx, |editor, cx| {
9546 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9547 });
9548 editor.update_in(cx, |editor, window, cx| {
9549 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9550 });
9551 editor.update(cx, |editor, cx| {
9552 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9553 });
9554 editor.update_in(cx, |editor, window, cx| {
9555 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9556 });
9557 editor.update(cx, |editor, cx| {
9558 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9559 });
9560
9561 // Test case 2: Cursor at end of statement
9562 editor.update_in(cx, |editor, window, cx| {
9563 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9564 s.select_display_ranges([
9565 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9566 ]);
9567 });
9568 });
9569 editor.update(cx, |editor, cx| {
9570 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9571 });
9572 editor.update_in(cx, |editor, window, cx| {
9573 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9574 });
9575 editor.update(cx, |editor, cx| {
9576 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9577 });
9578}
9579
9580#[gpui::test]
9581async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9582 init_test(cx, |_| {});
9583
9584 let language = Arc::new(Language::new(
9585 LanguageConfig {
9586 name: "JavaScript".into(),
9587 ..Default::default()
9588 },
9589 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9590 ));
9591
9592 let text = r#"
9593 let a = {
9594 key: "value",
9595 };
9596 "#
9597 .unindent();
9598
9599 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9600 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9601 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9602
9603 editor
9604 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9605 .await;
9606
9607 // Test case 1: Cursor after '{'
9608 editor.update_in(cx, |editor, window, cx| {
9609 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9610 s.select_display_ranges([
9611 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9612 ]);
9613 });
9614 });
9615 editor.update(cx, |editor, cx| {
9616 assert_text_with_selections(
9617 editor,
9618 indoc! {r#"
9619 let a = {ˇ
9620 key: "value",
9621 };
9622 "#},
9623 cx,
9624 );
9625 });
9626 editor.update_in(cx, |editor, window, cx| {
9627 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9628 });
9629 editor.update(cx, |editor, cx| {
9630 assert_text_with_selections(
9631 editor,
9632 indoc! {r#"
9633 let a = «ˇ{
9634 key: "value",
9635 }»;
9636 "#},
9637 cx,
9638 );
9639 });
9640
9641 // Test case 2: Cursor after ':'
9642 editor.update_in(cx, |editor, window, cx| {
9643 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9644 s.select_display_ranges([
9645 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9646 ]);
9647 });
9648 });
9649 editor.update(cx, |editor, cx| {
9650 assert_text_with_selections(
9651 editor,
9652 indoc! {r#"
9653 let a = {
9654 key:ˇ "value",
9655 };
9656 "#},
9657 cx,
9658 );
9659 });
9660 editor.update_in(cx, |editor, window, cx| {
9661 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9662 });
9663 editor.update(cx, |editor, cx| {
9664 assert_text_with_selections(
9665 editor,
9666 indoc! {r#"
9667 let a = {
9668 «ˇkey: "value"»,
9669 };
9670 "#},
9671 cx,
9672 );
9673 });
9674 editor.update_in(cx, |editor, window, cx| {
9675 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9676 });
9677 editor.update(cx, |editor, cx| {
9678 assert_text_with_selections(
9679 editor,
9680 indoc! {r#"
9681 let a = «ˇ{
9682 key: "value",
9683 }»;
9684 "#},
9685 cx,
9686 );
9687 });
9688
9689 // Test case 3: Cursor after ','
9690 editor.update_in(cx, |editor, window, cx| {
9691 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9692 s.select_display_ranges([
9693 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9694 ]);
9695 });
9696 });
9697 editor.update(cx, |editor, cx| {
9698 assert_text_with_selections(
9699 editor,
9700 indoc! {r#"
9701 let a = {
9702 key: "value",ˇ
9703 };
9704 "#},
9705 cx,
9706 );
9707 });
9708 editor.update_in(cx, |editor, window, cx| {
9709 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9710 });
9711 editor.update(cx, |editor, cx| {
9712 assert_text_with_selections(
9713 editor,
9714 indoc! {r#"
9715 let a = «ˇ{
9716 key: "value",
9717 }»;
9718 "#},
9719 cx,
9720 );
9721 });
9722
9723 // Test case 4: Cursor after ';'
9724 editor.update_in(cx, |editor, window, cx| {
9725 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9726 s.select_display_ranges([
9727 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9728 ]);
9729 });
9730 });
9731 editor.update(cx, |editor, cx| {
9732 assert_text_with_selections(
9733 editor,
9734 indoc! {r#"
9735 let a = {
9736 key: "value",
9737 };ˇ
9738 "#},
9739 cx,
9740 );
9741 });
9742 editor.update_in(cx, |editor, window, cx| {
9743 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9744 });
9745 editor.update(cx, |editor, cx| {
9746 assert_text_with_selections(
9747 editor,
9748 indoc! {r#"
9749 «ˇlet a = {
9750 key: "value",
9751 };
9752 »"#},
9753 cx,
9754 );
9755 });
9756}
9757
9758#[gpui::test]
9759async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9760 init_test(cx, |_| {});
9761
9762 let language = Arc::new(Language::new(
9763 LanguageConfig::default(),
9764 Some(tree_sitter_rust::LANGUAGE.into()),
9765 ));
9766
9767 let text = r#"
9768 use mod1::mod2::{mod3, mod4};
9769
9770 fn fn_1(param1: bool, param2: &str) {
9771 let var1 = "hello world";
9772 }
9773 "#
9774 .unindent();
9775
9776 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9777 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9778 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9779
9780 editor
9781 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9782 .await;
9783
9784 // Test 1: Cursor on a letter of a string word
9785 editor.update_in(cx, |editor, window, cx| {
9786 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9787 s.select_display_ranges([
9788 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9789 ]);
9790 });
9791 });
9792 editor.update_in(cx, |editor, window, cx| {
9793 assert_text_with_selections(
9794 editor,
9795 indoc! {r#"
9796 use mod1::mod2::{mod3, mod4};
9797
9798 fn fn_1(param1: bool, param2: &str) {
9799 let var1 = "hˇello world";
9800 }
9801 "#},
9802 cx,
9803 );
9804 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9805 assert_text_with_selections(
9806 editor,
9807 indoc! {r#"
9808 use mod1::mod2::{mod3, mod4};
9809
9810 fn fn_1(param1: bool, param2: &str) {
9811 let var1 = "«ˇhello» world";
9812 }
9813 "#},
9814 cx,
9815 );
9816 });
9817
9818 // Test 2: Partial selection within a word
9819 editor.update_in(cx, |editor, window, cx| {
9820 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9821 s.select_display_ranges([
9822 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9823 ]);
9824 });
9825 });
9826 editor.update_in(cx, |editor, window, cx| {
9827 assert_text_with_selections(
9828 editor,
9829 indoc! {r#"
9830 use mod1::mod2::{mod3, mod4};
9831
9832 fn fn_1(param1: bool, param2: &str) {
9833 let var1 = "h«elˇ»lo world";
9834 }
9835 "#},
9836 cx,
9837 );
9838 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9839 assert_text_with_selections(
9840 editor,
9841 indoc! {r#"
9842 use mod1::mod2::{mod3, mod4};
9843
9844 fn fn_1(param1: bool, param2: &str) {
9845 let var1 = "«ˇhello» world";
9846 }
9847 "#},
9848 cx,
9849 );
9850 });
9851
9852 // Test 3: Complete word already selected
9853 editor.update_in(cx, |editor, window, cx| {
9854 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9855 s.select_display_ranges([
9856 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9857 ]);
9858 });
9859 });
9860 editor.update_in(cx, |editor, window, cx| {
9861 assert_text_with_selections(
9862 editor,
9863 indoc! {r#"
9864 use mod1::mod2::{mod3, mod4};
9865
9866 fn fn_1(param1: bool, param2: &str) {
9867 let var1 = "«helloˇ» world";
9868 }
9869 "#},
9870 cx,
9871 );
9872 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9873 assert_text_with_selections(
9874 editor,
9875 indoc! {r#"
9876 use mod1::mod2::{mod3, mod4};
9877
9878 fn fn_1(param1: bool, param2: &str) {
9879 let var1 = "«hello worldˇ»";
9880 }
9881 "#},
9882 cx,
9883 );
9884 });
9885
9886 // Test 4: Selection spanning across words
9887 editor.update_in(cx, |editor, window, cx| {
9888 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9889 s.select_display_ranges([
9890 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9891 ]);
9892 });
9893 });
9894 editor.update_in(cx, |editor, window, cx| {
9895 assert_text_with_selections(
9896 editor,
9897 indoc! {r#"
9898 use mod1::mod2::{mod3, mod4};
9899
9900 fn fn_1(param1: bool, param2: &str) {
9901 let var1 = "hel«lo woˇ»rld";
9902 }
9903 "#},
9904 cx,
9905 );
9906 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9907 assert_text_with_selections(
9908 editor,
9909 indoc! {r#"
9910 use mod1::mod2::{mod3, mod4};
9911
9912 fn fn_1(param1: bool, param2: &str) {
9913 let var1 = "«ˇhello world»";
9914 }
9915 "#},
9916 cx,
9917 );
9918 });
9919
9920 // Test 5: Expansion beyond string
9921 editor.update_in(cx, |editor, window, cx| {
9922 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9923 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9924 assert_text_with_selections(
9925 editor,
9926 indoc! {r#"
9927 use mod1::mod2::{mod3, mod4};
9928
9929 fn fn_1(param1: bool, param2: &str) {
9930 «ˇlet var1 = "hello world";»
9931 }
9932 "#},
9933 cx,
9934 );
9935 });
9936}
9937
9938#[gpui::test]
9939async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9940 init_test(cx, |_| {});
9941
9942 let mut cx = EditorTestContext::new(cx).await;
9943
9944 let language = Arc::new(Language::new(
9945 LanguageConfig::default(),
9946 Some(tree_sitter_rust::LANGUAGE.into()),
9947 ));
9948
9949 cx.update_buffer(|buffer, cx| {
9950 buffer.set_language(Some(language), cx);
9951 });
9952
9953 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9954 cx.update_editor(|editor, window, cx| {
9955 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9956 });
9957
9958 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9959
9960 cx.set_state(indoc! { r#"fn a() {
9961 // what
9962 // a
9963 // ˇlong
9964 // method
9965 // I
9966 // sure
9967 // hope
9968 // it
9969 // works
9970 }"# });
9971
9972 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9973 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9974 cx.update(|_, cx| {
9975 multi_buffer.update(cx, |multi_buffer, cx| {
9976 multi_buffer.set_excerpts_for_path(
9977 PathKey::for_buffer(&buffer, cx),
9978 buffer,
9979 [Point::new(1, 0)..Point::new(1, 0)],
9980 3,
9981 cx,
9982 );
9983 });
9984 });
9985
9986 let editor2 = cx.new_window_entity(|window, cx| {
9987 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9988 });
9989
9990 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9991 cx.update_editor(|editor, window, cx| {
9992 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9993 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9994 })
9995 });
9996
9997 cx.assert_editor_state(indoc! { "
9998 fn a() {
9999 // what
10000 // a
10001 ˇ // long
10002 // method"});
10003
10004 cx.update_editor(|editor, window, cx| {
10005 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
10006 });
10007
10008 // Although we could potentially make the action work when the syntax node
10009 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
10010 // did. Maybe we could also expand the excerpt to contain the range?
10011 cx.assert_editor_state(indoc! { "
10012 fn a() {
10013 // what
10014 // a
10015 ˇ // long
10016 // method"});
10017}
10018
10019#[gpui::test]
10020async fn test_fold_function_bodies(cx: &mut TestAppContext) {
10021 init_test(cx, |_| {});
10022
10023 let base_text = r#"
10024 impl A {
10025 // this is an uncommitted comment
10026
10027 fn b() {
10028 c();
10029 }
10030
10031 // this is another uncommitted comment
10032
10033 fn d() {
10034 // e
10035 // f
10036 }
10037 }
10038
10039 fn g() {
10040 // h
10041 }
10042 "#
10043 .unindent();
10044
10045 let text = r#"
10046 ˇimpl A {
10047
10048 fn b() {
10049 c();
10050 }
10051
10052 fn d() {
10053 // e
10054 // f
10055 }
10056 }
10057
10058 fn g() {
10059 // h
10060 }
10061 "#
10062 .unindent();
10063
10064 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
10065 cx.set_state(&text);
10066 cx.set_head_text(&base_text);
10067 cx.update_editor(|editor, window, cx| {
10068 editor.expand_all_diff_hunks(&Default::default(), window, cx);
10069 });
10070
10071 cx.assert_state_with_diff(
10072 "
10073 ˇimpl A {
10074 - // this is an uncommitted comment
10075
10076 fn b() {
10077 c();
10078 }
10079
10080 - // this is another uncommitted comment
10081 -
10082 fn d() {
10083 // e
10084 // f
10085 }
10086 }
10087
10088 fn g() {
10089 // h
10090 }
10091 "
10092 .unindent(),
10093 );
10094
10095 let expected_display_text = "
10096 impl A {
10097 // this is an uncommitted comment
10098
10099 fn b() {
10100 ⋯
10101 }
10102
10103 // this is another uncommitted comment
10104
10105 fn d() {
10106 ⋯
10107 }
10108 }
10109
10110 fn g() {
10111 ⋯
10112 }
10113 "
10114 .unindent();
10115
10116 cx.update_editor(|editor, window, cx| {
10117 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
10118 assert_eq!(editor.display_text(cx), expected_display_text);
10119 });
10120}
10121
10122#[gpui::test]
10123async fn test_autoindent(cx: &mut TestAppContext) {
10124 init_test(cx, |_| {});
10125
10126 let language = Arc::new(
10127 Language::new(
10128 LanguageConfig {
10129 brackets: BracketPairConfig {
10130 pairs: vec![
10131 BracketPair {
10132 start: "{".to_string(),
10133 end: "}".to_string(),
10134 close: false,
10135 surround: false,
10136 newline: true,
10137 },
10138 BracketPair {
10139 start: "(".to_string(),
10140 end: ")".to_string(),
10141 close: false,
10142 surround: false,
10143 newline: true,
10144 },
10145 ],
10146 ..Default::default()
10147 },
10148 ..Default::default()
10149 },
10150 Some(tree_sitter_rust::LANGUAGE.into()),
10151 )
10152 .with_indents_query(
10153 r#"
10154 (_ "(" ")" @end) @indent
10155 (_ "{" "}" @end) @indent
10156 "#,
10157 )
10158 .unwrap(),
10159 );
10160
10161 let text = "fn a() {}";
10162
10163 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10164 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10165 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10166 editor
10167 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10168 .await;
10169
10170 editor.update_in(cx, |editor, window, cx| {
10171 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10172 s.select_ranges([
10173 MultiBufferOffset(5)..MultiBufferOffset(5),
10174 MultiBufferOffset(8)..MultiBufferOffset(8),
10175 MultiBufferOffset(9)..MultiBufferOffset(9),
10176 ])
10177 });
10178 editor.newline(&Newline, window, cx);
10179 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
10180 assert_eq!(
10181 editor.selections.ranges(&editor.display_snapshot(cx)),
10182 &[
10183 Point::new(1, 4)..Point::new(1, 4),
10184 Point::new(3, 4)..Point::new(3, 4),
10185 Point::new(5, 0)..Point::new(5, 0)
10186 ]
10187 );
10188 });
10189}
10190
10191#[gpui::test]
10192async fn test_autoindent_disabled(cx: &mut TestAppContext) {
10193 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
10194
10195 let language = Arc::new(
10196 Language::new(
10197 LanguageConfig {
10198 brackets: BracketPairConfig {
10199 pairs: vec![
10200 BracketPair {
10201 start: "{".to_string(),
10202 end: "}".to_string(),
10203 close: false,
10204 surround: false,
10205 newline: true,
10206 },
10207 BracketPair {
10208 start: "(".to_string(),
10209 end: ")".to_string(),
10210 close: false,
10211 surround: false,
10212 newline: true,
10213 },
10214 ],
10215 ..Default::default()
10216 },
10217 ..Default::default()
10218 },
10219 Some(tree_sitter_rust::LANGUAGE.into()),
10220 )
10221 .with_indents_query(
10222 r#"
10223 (_ "(" ")" @end) @indent
10224 (_ "{" "}" @end) @indent
10225 "#,
10226 )
10227 .unwrap(),
10228 );
10229
10230 let text = "fn a() {}";
10231
10232 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10233 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10234 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10235 editor
10236 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10237 .await;
10238
10239 editor.update_in(cx, |editor, window, cx| {
10240 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10241 s.select_ranges([
10242 MultiBufferOffset(5)..MultiBufferOffset(5),
10243 MultiBufferOffset(8)..MultiBufferOffset(8),
10244 MultiBufferOffset(9)..MultiBufferOffset(9),
10245 ])
10246 });
10247 editor.newline(&Newline, window, cx);
10248 assert_eq!(
10249 editor.text(cx),
10250 indoc!(
10251 "
10252 fn a(
10253
10254 ) {
10255
10256 }
10257 "
10258 )
10259 );
10260 assert_eq!(
10261 editor.selections.ranges(&editor.display_snapshot(cx)),
10262 &[
10263 Point::new(1, 0)..Point::new(1, 0),
10264 Point::new(3, 0)..Point::new(3, 0),
10265 Point::new(5, 0)..Point::new(5, 0)
10266 ]
10267 );
10268 });
10269}
10270
10271#[gpui::test]
10272async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
10273 init_test(cx, |settings| {
10274 settings.defaults.auto_indent = Some(true);
10275 settings.languages.0.insert(
10276 "python".into(),
10277 LanguageSettingsContent {
10278 auto_indent: Some(false),
10279 ..Default::default()
10280 },
10281 );
10282 });
10283
10284 let mut cx = EditorTestContext::new(cx).await;
10285
10286 let injected_language = Arc::new(
10287 Language::new(
10288 LanguageConfig {
10289 brackets: BracketPairConfig {
10290 pairs: vec![
10291 BracketPair {
10292 start: "{".to_string(),
10293 end: "}".to_string(),
10294 close: false,
10295 surround: false,
10296 newline: true,
10297 },
10298 BracketPair {
10299 start: "(".to_string(),
10300 end: ")".to_string(),
10301 close: true,
10302 surround: false,
10303 newline: true,
10304 },
10305 ],
10306 ..Default::default()
10307 },
10308 name: "python".into(),
10309 ..Default::default()
10310 },
10311 Some(tree_sitter_python::LANGUAGE.into()),
10312 )
10313 .with_indents_query(
10314 r#"
10315 (_ "(" ")" @end) @indent
10316 (_ "{" "}" @end) @indent
10317 "#,
10318 )
10319 .unwrap(),
10320 );
10321
10322 let language = Arc::new(
10323 Language::new(
10324 LanguageConfig {
10325 brackets: BracketPairConfig {
10326 pairs: vec![
10327 BracketPair {
10328 start: "{".to_string(),
10329 end: "}".to_string(),
10330 close: false,
10331 surround: false,
10332 newline: true,
10333 },
10334 BracketPair {
10335 start: "(".to_string(),
10336 end: ")".to_string(),
10337 close: true,
10338 surround: false,
10339 newline: true,
10340 },
10341 ],
10342 ..Default::default()
10343 },
10344 name: LanguageName::new_static("rust"),
10345 ..Default::default()
10346 },
10347 Some(tree_sitter_rust::LANGUAGE.into()),
10348 )
10349 .with_indents_query(
10350 r#"
10351 (_ "(" ")" @end) @indent
10352 (_ "{" "}" @end) @indent
10353 "#,
10354 )
10355 .unwrap()
10356 .with_injection_query(
10357 r#"
10358 (macro_invocation
10359 macro: (identifier) @_macro_name
10360 (token_tree) @injection.content
10361 (#set! injection.language "python"))
10362 "#,
10363 )
10364 .unwrap(),
10365 );
10366
10367 cx.language_registry().add(injected_language);
10368 cx.language_registry().add(language.clone());
10369
10370 cx.update_buffer(|buffer, cx| {
10371 buffer.set_language(Some(language), cx);
10372 });
10373
10374 cx.set_state(r#"struct A {ˇ}"#);
10375
10376 cx.update_editor(|editor, window, cx| {
10377 editor.newline(&Default::default(), window, cx);
10378 });
10379
10380 cx.assert_editor_state(indoc!(
10381 "struct A {
10382 ˇ
10383 }"
10384 ));
10385
10386 cx.set_state(r#"select_biased!(ˇ)"#);
10387
10388 cx.update_editor(|editor, window, cx| {
10389 editor.newline(&Default::default(), window, cx);
10390 editor.handle_input("def ", window, cx);
10391 editor.handle_input("(", window, cx);
10392 editor.newline(&Default::default(), window, cx);
10393 editor.handle_input("a", window, cx);
10394 });
10395
10396 cx.assert_editor_state(indoc!(
10397 "select_biased!(
10398 def (
10399 aˇ
10400 )
10401 )"
10402 ));
10403}
10404
10405#[gpui::test]
10406async fn test_autoindent_selections(cx: &mut TestAppContext) {
10407 init_test(cx, |_| {});
10408
10409 {
10410 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
10411 cx.set_state(indoc! {"
10412 impl A {
10413
10414 fn b() {}
10415
10416 «fn c() {
10417
10418 }ˇ»
10419 }
10420 "});
10421
10422 cx.update_editor(|editor, window, cx| {
10423 editor.autoindent(&Default::default(), window, cx);
10424 });
10425 cx.wait_for_autoindent_applied().await;
10426
10427 cx.assert_editor_state(indoc! {"
10428 impl A {
10429
10430 fn b() {}
10431
10432 «fn c() {
10433
10434 }ˇ»
10435 }
10436 "});
10437 }
10438
10439 {
10440 let mut cx = EditorTestContext::new_multibuffer(
10441 cx,
10442 [indoc! { "
10443 impl A {
10444 «
10445 // a
10446 fn b(){}
10447 »
10448 «
10449 }
10450 fn c(){}
10451 »
10452 "}],
10453 );
10454
10455 let buffer = cx.update_editor(|editor, _, cx| {
10456 let buffer = editor.buffer().update(cx, |buffer, _| {
10457 buffer.all_buffers().iter().next().unwrap().clone()
10458 });
10459 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
10460 buffer
10461 });
10462
10463 cx.run_until_parked();
10464 cx.update_editor(|editor, window, cx| {
10465 editor.select_all(&Default::default(), window, cx);
10466 editor.autoindent(&Default::default(), window, cx)
10467 });
10468 cx.run_until_parked();
10469
10470 cx.update(|_, cx| {
10471 assert_eq!(
10472 buffer.read(cx).text(),
10473 indoc! { "
10474 impl A {
10475
10476 // a
10477 fn b(){}
10478
10479
10480 }
10481 fn c(){}
10482
10483 " }
10484 )
10485 });
10486 }
10487}
10488
10489#[gpui::test]
10490async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
10491 init_test(cx, |_| {});
10492
10493 let mut cx = EditorTestContext::new(cx).await;
10494
10495 let language = Arc::new(Language::new(
10496 LanguageConfig {
10497 brackets: BracketPairConfig {
10498 pairs: vec![
10499 BracketPair {
10500 start: "{".to_string(),
10501 end: "}".to_string(),
10502 close: true,
10503 surround: true,
10504 newline: true,
10505 },
10506 BracketPair {
10507 start: "(".to_string(),
10508 end: ")".to_string(),
10509 close: true,
10510 surround: true,
10511 newline: true,
10512 },
10513 BracketPair {
10514 start: "/*".to_string(),
10515 end: " */".to_string(),
10516 close: true,
10517 surround: true,
10518 newline: true,
10519 },
10520 BracketPair {
10521 start: "[".to_string(),
10522 end: "]".to_string(),
10523 close: false,
10524 surround: false,
10525 newline: true,
10526 },
10527 BracketPair {
10528 start: "\"".to_string(),
10529 end: "\"".to_string(),
10530 close: true,
10531 surround: true,
10532 newline: false,
10533 },
10534 BracketPair {
10535 start: "<".to_string(),
10536 end: ">".to_string(),
10537 close: false,
10538 surround: true,
10539 newline: true,
10540 },
10541 ],
10542 ..Default::default()
10543 },
10544 autoclose_before: "})]".to_string(),
10545 ..Default::default()
10546 },
10547 Some(tree_sitter_rust::LANGUAGE.into()),
10548 ));
10549
10550 cx.language_registry().add(language.clone());
10551 cx.update_buffer(|buffer, cx| {
10552 buffer.set_language(Some(language), cx);
10553 });
10554
10555 cx.set_state(
10556 &r#"
10557 🏀ˇ
10558 εˇ
10559 ❤️ˇ
10560 "#
10561 .unindent(),
10562 );
10563
10564 // autoclose multiple nested brackets at multiple cursors
10565 cx.update_editor(|editor, window, cx| {
10566 editor.handle_input("{", window, cx);
10567 editor.handle_input("{", window, cx);
10568 editor.handle_input("{", window, cx);
10569 });
10570 cx.assert_editor_state(
10571 &"
10572 🏀{{{ˇ}}}
10573 ε{{{ˇ}}}
10574 ❤️{{{ˇ}}}
10575 "
10576 .unindent(),
10577 );
10578
10579 // insert a different closing bracket
10580 cx.update_editor(|editor, window, cx| {
10581 editor.handle_input(")", window, cx);
10582 });
10583 cx.assert_editor_state(
10584 &"
10585 🏀{{{)ˇ}}}
10586 ε{{{)ˇ}}}
10587 ❤️{{{)ˇ}}}
10588 "
10589 .unindent(),
10590 );
10591
10592 // skip over the auto-closed brackets when typing a closing bracket
10593 cx.update_editor(|editor, window, cx| {
10594 editor.move_right(&MoveRight, window, cx);
10595 editor.handle_input("}", window, cx);
10596 editor.handle_input("}", window, cx);
10597 editor.handle_input("}", window, cx);
10598 });
10599 cx.assert_editor_state(
10600 &"
10601 🏀{{{)}}}}ˇ
10602 ε{{{)}}}}ˇ
10603 ❤️{{{)}}}}ˇ
10604 "
10605 .unindent(),
10606 );
10607
10608 // autoclose multi-character pairs
10609 cx.set_state(
10610 &"
10611 ˇ
10612 ˇ
10613 "
10614 .unindent(),
10615 );
10616 cx.update_editor(|editor, window, cx| {
10617 editor.handle_input("/", window, cx);
10618 editor.handle_input("*", window, cx);
10619 });
10620 cx.assert_editor_state(
10621 &"
10622 /*ˇ */
10623 /*ˇ */
10624 "
10625 .unindent(),
10626 );
10627
10628 // one cursor autocloses a multi-character pair, one cursor
10629 // does not autoclose.
10630 cx.set_state(
10631 &"
10632 /ˇ
10633 ˇ
10634 "
10635 .unindent(),
10636 );
10637 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10638 cx.assert_editor_state(
10639 &"
10640 /*ˇ */
10641 *ˇ
10642 "
10643 .unindent(),
10644 );
10645
10646 // Don't autoclose if the next character isn't whitespace and isn't
10647 // listed in the language's "autoclose_before" section.
10648 cx.set_state("ˇa b");
10649 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10650 cx.assert_editor_state("{ˇa b");
10651
10652 // Don't autoclose if `close` is false for the bracket pair
10653 cx.set_state("ˇ");
10654 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10655 cx.assert_editor_state("[ˇ");
10656
10657 // Surround with brackets if text is selected
10658 cx.set_state("«aˇ» b");
10659 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10660 cx.assert_editor_state("{«aˇ»} b");
10661
10662 // Autoclose when not immediately after a word character
10663 cx.set_state("a ˇ");
10664 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10665 cx.assert_editor_state("a \"ˇ\"");
10666
10667 // Autoclose pair where the start and end characters are the same
10668 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10669 cx.assert_editor_state("a \"\"ˇ");
10670
10671 // Don't autoclose when immediately after a word character
10672 cx.set_state("aˇ");
10673 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10674 cx.assert_editor_state("a\"ˇ");
10675
10676 // Do autoclose when after a non-word character
10677 cx.set_state("{ˇ");
10678 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10679 cx.assert_editor_state("{\"ˇ\"");
10680
10681 // Non identical pairs autoclose regardless of preceding character
10682 cx.set_state("aˇ");
10683 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10684 cx.assert_editor_state("a{ˇ}");
10685
10686 // Don't autoclose pair if autoclose is disabled
10687 cx.set_state("ˇ");
10688 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10689 cx.assert_editor_state("<ˇ");
10690
10691 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10692 cx.set_state("«aˇ» b");
10693 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10694 cx.assert_editor_state("<«aˇ»> b");
10695}
10696
10697#[gpui::test]
10698async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10699 init_test(cx, |settings| {
10700 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10701 });
10702
10703 let mut cx = EditorTestContext::new(cx).await;
10704
10705 let language = Arc::new(Language::new(
10706 LanguageConfig {
10707 brackets: BracketPairConfig {
10708 pairs: vec![
10709 BracketPair {
10710 start: "{".to_string(),
10711 end: "}".to_string(),
10712 close: true,
10713 surround: true,
10714 newline: true,
10715 },
10716 BracketPair {
10717 start: "(".to_string(),
10718 end: ")".to_string(),
10719 close: true,
10720 surround: true,
10721 newline: true,
10722 },
10723 BracketPair {
10724 start: "[".to_string(),
10725 end: "]".to_string(),
10726 close: false,
10727 surround: false,
10728 newline: true,
10729 },
10730 ],
10731 ..Default::default()
10732 },
10733 autoclose_before: "})]".to_string(),
10734 ..Default::default()
10735 },
10736 Some(tree_sitter_rust::LANGUAGE.into()),
10737 ));
10738
10739 cx.language_registry().add(language.clone());
10740 cx.update_buffer(|buffer, cx| {
10741 buffer.set_language(Some(language), cx);
10742 });
10743
10744 cx.set_state(
10745 &"
10746 ˇ
10747 ˇ
10748 ˇ
10749 "
10750 .unindent(),
10751 );
10752
10753 // ensure only matching closing brackets are skipped over
10754 cx.update_editor(|editor, window, cx| {
10755 editor.handle_input("}", window, cx);
10756 editor.move_left(&MoveLeft, window, cx);
10757 editor.handle_input(")", window, cx);
10758 editor.move_left(&MoveLeft, window, cx);
10759 });
10760 cx.assert_editor_state(
10761 &"
10762 ˇ)}
10763 ˇ)}
10764 ˇ)}
10765 "
10766 .unindent(),
10767 );
10768
10769 // skip-over closing brackets at multiple cursors
10770 cx.update_editor(|editor, window, cx| {
10771 editor.handle_input(")", window, cx);
10772 editor.handle_input("}", window, cx);
10773 });
10774 cx.assert_editor_state(
10775 &"
10776 )}ˇ
10777 )}ˇ
10778 )}ˇ
10779 "
10780 .unindent(),
10781 );
10782
10783 // ignore non-close brackets
10784 cx.update_editor(|editor, window, cx| {
10785 editor.handle_input("]", window, cx);
10786 editor.move_left(&MoveLeft, window, cx);
10787 editor.handle_input("]", window, cx);
10788 });
10789 cx.assert_editor_state(
10790 &"
10791 )}]ˇ]
10792 )}]ˇ]
10793 )}]ˇ]
10794 "
10795 .unindent(),
10796 );
10797}
10798
10799#[gpui::test]
10800async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10801 init_test(cx, |_| {});
10802
10803 let mut cx = EditorTestContext::new(cx).await;
10804
10805 let html_language = Arc::new(
10806 Language::new(
10807 LanguageConfig {
10808 name: "HTML".into(),
10809 brackets: BracketPairConfig {
10810 pairs: vec![
10811 BracketPair {
10812 start: "<".into(),
10813 end: ">".into(),
10814 close: true,
10815 ..Default::default()
10816 },
10817 BracketPair {
10818 start: "{".into(),
10819 end: "}".into(),
10820 close: true,
10821 ..Default::default()
10822 },
10823 BracketPair {
10824 start: "(".into(),
10825 end: ")".into(),
10826 close: true,
10827 ..Default::default()
10828 },
10829 ],
10830 ..Default::default()
10831 },
10832 autoclose_before: "})]>".into(),
10833 ..Default::default()
10834 },
10835 Some(tree_sitter_html::LANGUAGE.into()),
10836 )
10837 .with_injection_query(
10838 r#"
10839 (script_element
10840 (raw_text) @injection.content
10841 (#set! injection.language "javascript"))
10842 "#,
10843 )
10844 .unwrap(),
10845 );
10846
10847 let javascript_language = Arc::new(Language::new(
10848 LanguageConfig {
10849 name: "JavaScript".into(),
10850 brackets: BracketPairConfig {
10851 pairs: vec![
10852 BracketPair {
10853 start: "/*".into(),
10854 end: " */".into(),
10855 close: true,
10856 ..Default::default()
10857 },
10858 BracketPair {
10859 start: "{".into(),
10860 end: "}".into(),
10861 close: true,
10862 ..Default::default()
10863 },
10864 BracketPair {
10865 start: "(".into(),
10866 end: ")".into(),
10867 close: true,
10868 ..Default::default()
10869 },
10870 ],
10871 ..Default::default()
10872 },
10873 autoclose_before: "})]>".into(),
10874 ..Default::default()
10875 },
10876 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10877 ));
10878
10879 cx.language_registry().add(html_language.clone());
10880 cx.language_registry().add(javascript_language);
10881 cx.executor().run_until_parked();
10882
10883 cx.update_buffer(|buffer, cx| {
10884 buffer.set_language(Some(html_language), cx);
10885 });
10886
10887 cx.set_state(
10888 &r#"
10889 <body>ˇ
10890 <script>
10891 var x = 1;ˇ
10892 </script>
10893 </body>ˇ
10894 "#
10895 .unindent(),
10896 );
10897
10898 // Precondition: different languages are active at different locations.
10899 cx.update_editor(|editor, window, cx| {
10900 let snapshot = editor.snapshot(window, cx);
10901 let cursors = editor
10902 .selections
10903 .ranges::<MultiBufferOffset>(&editor.display_snapshot(cx));
10904 let languages = cursors
10905 .iter()
10906 .map(|c| snapshot.language_at(c.start).unwrap().name())
10907 .collect::<Vec<_>>();
10908 assert_eq!(
10909 languages,
10910 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10911 );
10912 });
10913
10914 // Angle brackets autoclose in HTML, but not JavaScript.
10915 cx.update_editor(|editor, window, cx| {
10916 editor.handle_input("<", window, cx);
10917 editor.handle_input("a", window, cx);
10918 });
10919 cx.assert_editor_state(
10920 &r#"
10921 <body><aˇ>
10922 <script>
10923 var x = 1;<aˇ
10924 </script>
10925 </body><aˇ>
10926 "#
10927 .unindent(),
10928 );
10929
10930 // Curly braces and parens autoclose in both HTML and JavaScript.
10931 cx.update_editor(|editor, window, cx| {
10932 editor.handle_input(" b=", window, cx);
10933 editor.handle_input("{", window, cx);
10934 editor.handle_input("c", window, cx);
10935 editor.handle_input("(", window, cx);
10936 });
10937 cx.assert_editor_state(
10938 &r#"
10939 <body><a b={c(ˇ)}>
10940 <script>
10941 var x = 1;<a b={c(ˇ)}
10942 </script>
10943 </body><a b={c(ˇ)}>
10944 "#
10945 .unindent(),
10946 );
10947
10948 // Brackets that were already autoclosed are skipped.
10949 cx.update_editor(|editor, window, cx| {
10950 editor.handle_input(")", window, cx);
10951 editor.handle_input("d", window, cx);
10952 editor.handle_input("}", window, cx);
10953 });
10954 cx.assert_editor_state(
10955 &r#"
10956 <body><a b={c()d}ˇ>
10957 <script>
10958 var x = 1;<a b={c()d}ˇ
10959 </script>
10960 </body><a b={c()d}ˇ>
10961 "#
10962 .unindent(),
10963 );
10964 cx.update_editor(|editor, window, cx| {
10965 editor.handle_input(">", window, cx);
10966 });
10967 cx.assert_editor_state(
10968 &r#"
10969 <body><a b={c()d}>ˇ
10970 <script>
10971 var x = 1;<a b={c()d}>ˇ
10972 </script>
10973 </body><a b={c()d}>ˇ
10974 "#
10975 .unindent(),
10976 );
10977
10978 // Reset
10979 cx.set_state(
10980 &r#"
10981 <body>ˇ
10982 <script>
10983 var x = 1;ˇ
10984 </script>
10985 </body>ˇ
10986 "#
10987 .unindent(),
10988 );
10989
10990 cx.update_editor(|editor, window, cx| {
10991 editor.handle_input("<", window, cx);
10992 });
10993 cx.assert_editor_state(
10994 &r#"
10995 <body><ˇ>
10996 <script>
10997 var x = 1;<ˇ
10998 </script>
10999 </body><ˇ>
11000 "#
11001 .unindent(),
11002 );
11003
11004 // When backspacing, the closing angle brackets are removed.
11005 cx.update_editor(|editor, window, cx| {
11006 editor.backspace(&Backspace, window, cx);
11007 });
11008 cx.assert_editor_state(
11009 &r#"
11010 <body>ˇ
11011 <script>
11012 var x = 1;ˇ
11013 </script>
11014 </body>ˇ
11015 "#
11016 .unindent(),
11017 );
11018
11019 // Block comments autoclose in JavaScript, but not HTML.
11020 cx.update_editor(|editor, window, cx| {
11021 editor.handle_input("/", window, cx);
11022 editor.handle_input("*", window, cx);
11023 });
11024 cx.assert_editor_state(
11025 &r#"
11026 <body>/*ˇ
11027 <script>
11028 var x = 1;/*ˇ */
11029 </script>
11030 </body>/*ˇ
11031 "#
11032 .unindent(),
11033 );
11034}
11035
11036#[gpui::test]
11037async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
11038 init_test(cx, |_| {});
11039
11040 let mut cx = EditorTestContext::new(cx).await;
11041
11042 let rust_language = Arc::new(
11043 Language::new(
11044 LanguageConfig {
11045 name: "Rust".into(),
11046 brackets: serde_json::from_value(json!([
11047 { "start": "{", "end": "}", "close": true, "newline": true },
11048 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
11049 ]))
11050 .unwrap(),
11051 autoclose_before: "})]>".into(),
11052 ..Default::default()
11053 },
11054 Some(tree_sitter_rust::LANGUAGE.into()),
11055 )
11056 .with_override_query("(string_literal) @string")
11057 .unwrap(),
11058 );
11059
11060 cx.language_registry().add(rust_language.clone());
11061 cx.update_buffer(|buffer, cx| {
11062 buffer.set_language(Some(rust_language), cx);
11063 });
11064
11065 cx.set_state(
11066 &r#"
11067 let x = ˇ
11068 "#
11069 .unindent(),
11070 );
11071
11072 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
11073 cx.update_editor(|editor, window, cx| {
11074 editor.handle_input("\"", window, cx);
11075 });
11076 cx.assert_editor_state(
11077 &r#"
11078 let x = "ˇ"
11079 "#
11080 .unindent(),
11081 );
11082
11083 // Inserting another quotation mark. The cursor moves across the existing
11084 // automatically-inserted quotation mark.
11085 cx.update_editor(|editor, window, cx| {
11086 editor.handle_input("\"", window, cx);
11087 });
11088 cx.assert_editor_state(
11089 &r#"
11090 let x = ""ˇ
11091 "#
11092 .unindent(),
11093 );
11094
11095 // Reset
11096 cx.set_state(
11097 &r#"
11098 let x = ˇ
11099 "#
11100 .unindent(),
11101 );
11102
11103 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
11104 cx.update_editor(|editor, window, cx| {
11105 editor.handle_input("\"", window, cx);
11106 editor.handle_input(" ", window, cx);
11107 editor.move_left(&Default::default(), window, cx);
11108 editor.handle_input("\\", window, cx);
11109 editor.handle_input("\"", window, cx);
11110 });
11111 cx.assert_editor_state(
11112 &r#"
11113 let x = "\"ˇ "
11114 "#
11115 .unindent(),
11116 );
11117
11118 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
11119 // mark. Nothing is inserted.
11120 cx.update_editor(|editor, window, cx| {
11121 editor.move_right(&Default::default(), window, cx);
11122 editor.handle_input("\"", window, cx);
11123 });
11124 cx.assert_editor_state(
11125 &r#"
11126 let x = "\" "ˇ
11127 "#
11128 .unindent(),
11129 );
11130}
11131
11132#[gpui::test]
11133async fn test_autoclose_quotes_with_scope_awareness(cx: &mut TestAppContext) {
11134 init_test(cx, |_| {});
11135
11136 let mut cx = EditorTestContext::new(cx).await;
11137 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
11138
11139 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11140
11141 // Double quote inside single-quoted string
11142 cx.set_state(indoc! {r#"
11143 def main():
11144 items = ['"', ˇ]
11145 "#});
11146 cx.update_editor(|editor, window, cx| {
11147 editor.handle_input("\"", window, cx);
11148 });
11149 cx.assert_editor_state(indoc! {r#"
11150 def main():
11151 items = ['"', "ˇ"]
11152 "#});
11153
11154 // Two double quotes inside single-quoted string
11155 cx.set_state(indoc! {r#"
11156 def main():
11157 items = ['""', ˇ]
11158 "#});
11159 cx.update_editor(|editor, window, cx| {
11160 editor.handle_input("\"", window, cx);
11161 });
11162 cx.assert_editor_state(indoc! {r#"
11163 def main():
11164 items = ['""', "ˇ"]
11165 "#});
11166
11167 // Single quote inside double-quoted string
11168 cx.set_state(indoc! {r#"
11169 def main():
11170 items = ["'", ˇ]
11171 "#});
11172 cx.update_editor(|editor, window, cx| {
11173 editor.handle_input("'", window, cx);
11174 });
11175 cx.assert_editor_state(indoc! {r#"
11176 def main():
11177 items = ["'", 'ˇ']
11178 "#});
11179
11180 // Two single quotes inside double-quoted string
11181 cx.set_state(indoc! {r#"
11182 def main():
11183 items = ["''", ˇ]
11184 "#});
11185 cx.update_editor(|editor, window, cx| {
11186 editor.handle_input("'", window, cx);
11187 });
11188 cx.assert_editor_state(indoc! {r#"
11189 def main():
11190 items = ["''", 'ˇ']
11191 "#});
11192
11193 // Mixed quotes on same line
11194 cx.set_state(indoc! {r#"
11195 def main():
11196 items = ['"""', "'''''", ˇ]
11197 "#});
11198 cx.update_editor(|editor, window, cx| {
11199 editor.handle_input("\"", window, cx);
11200 });
11201 cx.assert_editor_state(indoc! {r#"
11202 def main():
11203 items = ['"""', "'''''", "ˇ"]
11204 "#});
11205 cx.update_editor(|editor, window, cx| {
11206 editor.move_right(&MoveRight, window, cx);
11207 });
11208 cx.update_editor(|editor, window, cx| {
11209 editor.handle_input(", ", window, cx);
11210 });
11211 cx.update_editor(|editor, window, cx| {
11212 editor.handle_input("'", window, cx);
11213 });
11214 cx.assert_editor_state(indoc! {r#"
11215 def main():
11216 items = ['"""', "'''''", "", 'ˇ']
11217 "#});
11218}
11219
11220#[gpui::test]
11221async fn test_autoclose_quotes_with_multibyte_characters(cx: &mut TestAppContext) {
11222 init_test(cx, |_| {});
11223
11224 let mut cx = EditorTestContext::new(cx).await;
11225 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
11226 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11227
11228 cx.set_state(indoc! {r#"
11229 def main():
11230 items = ["🎉", ˇ]
11231 "#});
11232 cx.update_editor(|editor, window, cx| {
11233 editor.handle_input("\"", window, cx);
11234 });
11235 cx.assert_editor_state(indoc! {r#"
11236 def main():
11237 items = ["🎉", "ˇ"]
11238 "#});
11239}
11240
11241#[gpui::test]
11242async fn test_surround_with_pair(cx: &mut TestAppContext) {
11243 init_test(cx, |_| {});
11244
11245 let language = Arc::new(Language::new(
11246 LanguageConfig {
11247 brackets: BracketPairConfig {
11248 pairs: vec![
11249 BracketPair {
11250 start: "{".to_string(),
11251 end: "}".to_string(),
11252 close: true,
11253 surround: true,
11254 newline: true,
11255 },
11256 BracketPair {
11257 start: "/* ".to_string(),
11258 end: "*/".to_string(),
11259 close: true,
11260 surround: true,
11261 ..Default::default()
11262 },
11263 ],
11264 ..Default::default()
11265 },
11266 ..Default::default()
11267 },
11268 Some(tree_sitter_rust::LANGUAGE.into()),
11269 ));
11270
11271 let text = r#"
11272 a
11273 b
11274 c
11275 "#
11276 .unindent();
11277
11278 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11279 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11280 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11281 editor
11282 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11283 .await;
11284
11285 editor.update_in(cx, |editor, window, cx| {
11286 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11287 s.select_display_ranges([
11288 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11289 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11290 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
11291 ])
11292 });
11293
11294 editor.handle_input("{", window, cx);
11295 editor.handle_input("{", window, cx);
11296 editor.handle_input("{", window, cx);
11297 assert_eq!(
11298 editor.text(cx),
11299 "
11300 {{{a}}}
11301 {{{b}}}
11302 {{{c}}}
11303 "
11304 .unindent()
11305 );
11306 assert_eq!(
11307 display_ranges(editor, cx),
11308 [
11309 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
11310 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
11311 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
11312 ]
11313 );
11314
11315 editor.undo(&Undo, window, cx);
11316 editor.undo(&Undo, window, cx);
11317 editor.undo(&Undo, window, cx);
11318 assert_eq!(
11319 editor.text(cx),
11320 "
11321 a
11322 b
11323 c
11324 "
11325 .unindent()
11326 );
11327 assert_eq!(
11328 display_ranges(editor, cx),
11329 [
11330 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11331 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11332 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
11333 ]
11334 );
11335
11336 // Ensure inserting the first character of a multi-byte bracket pair
11337 // doesn't surround the selections with the bracket.
11338 editor.handle_input("/", window, cx);
11339 assert_eq!(
11340 editor.text(cx),
11341 "
11342 /
11343 /
11344 /
11345 "
11346 .unindent()
11347 );
11348 assert_eq!(
11349 display_ranges(editor, cx),
11350 [
11351 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
11352 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
11353 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
11354 ]
11355 );
11356
11357 editor.undo(&Undo, window, cx);
11358 assert_eq!(
11359 editor.text(cx),
11360 "
11361 a
11362 b
11363 c
11364 "
11365 .unindent()
11366 );
11367 assert_eq!(
11368 display_ranges(editor, cx),
11369 [
11370 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
11371 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
11372 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
11373 ]
11374 );
11375
11376 // Ensure inserting the last character of a multi-byte bracket pair
11377 // doesn't surround the selections with the bracket.
11378 editor.handle_input("*", window, cx);
11379 assert_eq!(
11380 editor.text(cx),
11381 "
11382 *
11383 *
11384 *
11385 "
11386 .unindent()
11387 );
11388 assert_eq!(
11389 display_ranges(editor, cx),
11390 [
11391 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
11392 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
11393 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
11394 ]
11395 );
11396 });
11397}
11398
11399#[gpui::test]
11400async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
11401 init_test(cx, |_| {});
11402
11403 let language = Arc::new(Language::new(
11404 LanguageConfig {
11405 brackets: BracketPairConfig {
11406 pairs: vec![BracketPair {
11407 start: "{".to_string(),
11408 end: "}".to_string(),
11409 close: true,
11410 surround: true,
11411 newline: true,
11412 }],
11413 ..Default::default()
11414 },
11415 autoclose_before: "}".to_string(),
11416 ..Default::default()
11417 },
11418 Some(tree_sitter_rust::LANGUAGE.into()),
11419 ));
11420
11421 let text = r#"
11422 a
11423 b
11424 c
11425 "#
11426 .unindent();
11427
11428 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11429 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11430 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11431 editor
11432 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11433 .await;
11434
11435 editor.update_in(cx, |editor, window, cx| {
11436 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
11437 s.select_ranges([
11438 Point::new(0, 1)..Point::new(0, 1),
11439 Point::new(1, 1)..Point::new(1, 1),
11440 Point::new(2, 1)..Point::new(2, 1),
11441 ])
11442 });
11443
11444 editor.handle_input("{", window, cx);
11445 editor.handle_input("{", window, cx);
11446 editor.handle_input("_", window, cx);
11447 assert_eq!(
11448 editor.text(cx),
11449 "
11450 a{{_}}
11451 b{{_}}
11452 c{{_}}
11453 "
11454 .unindent()
11455 );
11456 assert_eq!(
11457 editor
11458 .selections
11459 .ranges::<Point>(&editor.display_snapshot(cx)),
11460 [
11461 Point::new(0, 4)..Point::new(0, 4),
11462 Point::new(1, 4)..Point::new(1, 4),
11463 Point::new(2, 4)..Point::new(2, 4)
11464 ]
11465 );
11466
11467 editor.backspace(&Default::default(), window, cx);
11468 editor.backspace(&Default::default(), window, cx);
11469 assert_eq!(
11470 editor.text(cx),
11471 "
11472 a{}
11473 b{}
11474 c{}
11475 "
11476 .unindent()
11477 );
11478 assert_eq!(
11479 editor
11480 .selections
11481 .ranges::<Point>(&editor.display_snapshot(cx)),
11482 [
11483 Point::new(0, 2)..Point::new(0, 2),
11484 Point::new(1, 2)..Point::new(1, 2),
11485 Point::new(2, 2)..Point::new(2, 2)
11486 ]
11487 );
11488
11489 editor.delete_to_previous_word_start(&Default::default(), window, cx);
11490 assert_eq!(
11491 editor.text(cx),
11492 "
11493 a
11494 b
11495 c
11496 "
11497 .unindent()
11498 );
11499 assert_eq!(
11500 editor
11501 .selections
11502 .ranges::<Point>(&editor.display_snapshot(cx)),
11503 [
11504 Point::new(0, 1)..Point::new(0, 1),
11505 Point::new(1, 1)..Point::new(1, 1),
11506 Point::new(2, 1)..Point::new(2, 1)
11507 ]
11508 );
11509 });
11510}
11511
11512#[gpui::test]
11513async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
11514 init_test(cx, |settings| {
11515 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
11516 });
11517
11518 let mut cx = EditorTestContext::new(cx).await;
11519
11520 let language = Arc::new(Language::new(
11521 LanguageConfig {
11522 brackets: BracketPairConfig {
11523 pairs: vec![
11524 BracketPair {
11525 start: "{".to_string(),
11526 end: "}".to_string(),
11527 close: true,
11528 surround: true,
11529 newline: true,
11530 },
11531 BracketPair {
11532 start: "(".to_string(),
11533 end: ")".to_string(),
11534 close: true,
11535 surround: true,
11536 newline: true,
11537 },
11538 BracketPair {
11539 start: "[".to_string(),
11540 end: "]".to_string(),
11541 close: false,
11542 surround: true,
11543 newline: true,
11544 },
11545 ],
11546 ..Default::default()
11547 },
11548 autoclose_before: "})]".to_string(),
11549 ..Default::default()
11550 },
11551 Some(tree_sitter_rust::LANGUAGE.into()),
11552 ));
11553
11554 cx.language_registry().add(language.clone());
11555 cx.update_buffer(|buffer, cx| {
11556 buffer.set_language(Some(language), cx);
11557 });
11558
11559 cx.set_state(
11560 &"
11561 {(ˇ)}
11562 [[ˇ]]
11563 {(ˇ)}
11564 "
11565 .unindent(),
11566 );
11567
11568 cx.update_editor(|editor, window, cx| {
11569 editor.backspace(&Default::default(), window, cx);
11570 editor.backspace(&Default::default(), window, cx);
11571 });
11572
11573 cx.assert_editor_state(
11574 &"
11575 ˇ
11576 ˇ]]
11577 ˇ
11578 "
11579 .unindent(),
11580 );
11581
11582 cx.update_editor(|editor, window, cx| {
11583 editor.handle_input("{", window, cx);
11584 editor.handle_input("{", window, cx);
11585 editor.move_right(&MoveRight, window, cx);
11586 editor.move_right(&MoveRight, window, cx);
11587 editor.move_left(&MoveLeft, window, cx);
11588 editor.move_left(&MoveLeft, window, cx);
11589 editor.backspace(&Default::default(), window, cx);
11590 });
11591
11592 cx.assert_editor_state(
11593 &"
11594 {ˇ}
11595 {ˇ}]]
11596 {ˇ}
11597 "
11598 .unindent(),
11599 );
11600
11601 cx.update_editor(|editor, window, cx| {
11602 editor.backspace(&Default::default(), window, cx);
11603 });
11604
11605 cx.assert_editor_state(
11606 &"
11607 ˇ
11608 ˇ]]
11609 ˇ
11610 "
11611 .unindent(),
11612 );
11613}
11614
11615#[gpui::test]
11616async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
11617 init_test(cx, |_| {});
11618
11619 let language = Arc::new(Language::new(
11620 LanguageConfig::default(),
11621 Some(tree_sitter_rust::LANGUAGE.into()),
11622 ));
11623
11624 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
11625 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11626 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11627 editor
11628 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11629 .await;
11630
11631 editor.update_in(cx, |editor, window, cx| {
11632 editor.set_auto_replace_emoji_shortcode(true);
11633
11634 editor.handle_input("Hello ", window, cx);
11635 editor.handle_input(":wave", window, cx);
11636 assert_eq!(editor.text(cx), "Hello :wave".unindent());
11637
11638 editor.handle_input(":", window, cx);
11639 assert_eq!(editor.text(cx), "Hello 👋".unindent());
11640
11641 editor.handle_input(" :smile", window, cx);
11642 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
11643
11644 editor.handle_input(":", window, cx);
11645 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
11646
11647 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
11648 editor.handle_input(":wave", window, cx);
11649 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
11650
11651 editor.handle_input(":", window, cx);
11652 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
11653
11654 editor.handle_input(":1", window, cx);
11655 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
11656
11657 editor.handle_input(":", window, cx);
11658 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11659
11660 // Ensure shortcode does not get replaced when it is part of a word
11661 editor.handle_input(" Test:wave", window, cx);
11662 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11663
11664 editor.handle_input(":", window, cx);
11665 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11666
11667 editor.set_auto_replace_emoji_shortcode(false);
11668
11669 // Ensure shortcode does not get replaced when auto replace is off
11670 editor.handle_input(" :wave", window, cx);
11671 assert_eq!(
11672 editor.text(cx),
11673 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11674 );
11675
11676 editor.handle_input(":", window, cx);
11677 assert_eq!(
11678 editor.text(cx),
11679 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11680 );
11681 });
11682}
11683
11684#[gpui::test]
11685async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11686 init_test(cx, |_| {});
11687
11688 let (text, insertion_ranges) = marked_text_ranges(
11689 indoc! {"
11690 ˇ
11691 "},
11692 false,
11693 );
11694
11695 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11696 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11697
11698 _ = editor.update_in(cx, |editor, window, cx| {
11699 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11700
11701 editor
11702 .insert_snippet(
11703 &insertion_ranges
11704 .iter()
11705 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11706 .collect::<Vec<_>>(),
11707 snippet,
11708 window,
11709 cx,
11710 )
11711 .unwrap();
11712
11713 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11714 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11715 assert_eq!(editor.text(cx), expected_text);
11716 assert_eq!(
11717 editor.selections.ranges(&editor.display_snapshot(cx)),
11718 selection_ranges
11719 .iter()
11720 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11721 .collect::<Vec<_>>()
11722 );
11723 }
11724
11725 assert(
11726 editor,
11727 cx,
11728 indoc! {"
11729 type «» =•
11730 "},
11731 );
11732
11733 assert!(editor.context_menu_visible(), "There should be a matches");
11734 });
11735}
11736
11737#[gpui::test]
11738async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
11739 init_test(cx, |_| {});
11740
11741 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11742 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11743 assert_eq!(editor.text(cx), expected_text);
11744 assert_eq!(
11745 editor.selections.ranges(&editor.display_snapshot(cx)),
11746 selection_ranges
11747 .iter()
11748 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11749 .collect::<Vec<_>>()
11750 );
11751 }
11752
11753 let (text, insertion_ranges) = marked_text_ranges(
11754 indoc! {"
11755 ˇ
11756 "},
11757 false,
11758 );
11759
11760 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11761 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11762
11763 _ = editor.update_in(cx, |editor, window, cx| {
11764 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
11765
11766 editor
11767 .insert_snippet(
11768 &insertion_ranges
11769 .iter()
11770 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
11771 .collect::<Vec<_>>(),
11772 snippet,
11773 window,
11774 cx,
11775 )
11776 .unwrap();
11777
11778 assert_state(
11779 editor,
11780 cx,
11781 indoc! {"
11782 type «» = ;•
11783 "},
11784 );
11785
11786 assert!(
11787 editor.context_menu_visible(),
11788 "Context menu should be visible for placeholder choices"
11789 );
11790
11791 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11792
11793 assert_state(
11794 editor,
11795 cx,
11796 indoc! {"
11797 type = «»;•
11798 "},
11799 );
11800
11801 assert!(
11802 !editor.context_menu_visible(),
11803 "Context menu should be hidden after moving to next tabstop"
11804 );
11805
11806 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11807
11808 assert_state(
11809 editor,
11810 cx,
11811 indoc! {"
11812 type = ; ˇ
11813 "},
11814 );
11815
11816 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11817
11818 assert_state(
11819 editor,
11820 cx,
11821 indoc! {"
11822 type = ; ˇ
11823 "},
11824 );
11825 });
11826
11827 _ = editor.update_in(cx, |editor, window, cx| {
11828 editor.select_all(&SelectAll, window, cx);
11829 editor.backspace(&Backspace, window, cx);
11830
11831 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
11832 let insertion_ranges = editor
11833 .selections
11834 .all(&editor.display_snapshot(cx))
11835 .iter()
11836 .map(|s| s.range())
11837 .collect::<Vec<_>>();
11838
11839 editor
11840 .insert_snippet(&insertion_ranges, snippet, window, cx)
11841 .unwrap();
11842
11843 assert_state(editor, cx, "fn «» = value;•");
11844
11845 assert!(
11846 editor.context_menu_visible(),
11847 "Context menu should be visible for placeholder choices"
11848 );
11849
11850 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11851
11852 assert_state(editor, cx, "fn = «valueˇ»;•");
11853
11854 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11855
11856 assert_state(editor, cx, "fn «» = value;•");
11857
11858 assert!(
11859 editor.context_menu_visible(),
11860 "Context menu should be visible again after returning to first tabstop"
11861 );
11862
11863 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11864
11865 assert_state(editor, cx, "fn «» = value;•");
11866 });
11867}
11868
11869#[gpui::test]
11870async fn test_snippets(cx: &mut TestAppContext) {
11871 init_test(cx, |_| {});
11872
11873 let mut cx = EditorTestContext::new(cx).await;
11874
11875 cx.set_state(indoc! {"
11876 a.ˇ b
11877 a.ˇ b
11878 a.ˇ b
11879 "});
11880
11881 cx.update_editor(|editor, window, cx| {
11882 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11883 let insertion_ranges = editor
11884 .selections
11885 .all(&editor.display_snapshot(cx))
11886 .iter()
11887 .map(|s| s.range())
11888 .collect::<Vec<_>>();
11889 editor
11890 .insert_snippet(&insertion_ranges, snippet, window, cx)
11891 .unwrap();
11892 });
11893
11894 cx.assert_editor_state(indoc! {"
11895 a.f(«oneˇ», two, «threeˇ») b
11896 a.f(«oneˇ», two, «threeˇ») b
11897 a.f(«oneˇ», two, «threeˇ») b
11898 "});
11899
11900 // Can't move earlier than the first tab stop
11901 cx.update_editor(|editor, window, cx| {
11902 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11903 });
11904 cx.assert_editor_state(indoc! {"
11905 a.f(«oneˇ», two, «threeˇ») b
11906 a.f(«oneˇ», two, «threeˇ») b
11907 a.f(«oneˇ», two, «threeˇ») b
11908 "});
11909
11910 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11911 cx.assert_editor_state(indoc! {"
11912 a.f(one, «twoˇ», three) b
11913 a.f(one, «twoˇ», three) b
11914 a.f(one, «twoˇ», three) b
11915 "});
11916
11917 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11918 cx.assert_editor_state(indoc! {"
11919 a.f(«oneˇ», two, «threeˇ») b
11920 a.f(«oneˇ», two, «threeˇ») b
11921 a.f(«oneˇ», two, «threeˇ») b
11922 "});
11923
11924 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11925 cx.assert_editor_state(indoc! {"
11926 a.f(one, «twoˇ», three) b
11927 a.f(one, «twoˇ», three) b
11928 a.f(one, «twoˇ», three) b
11929 "});
11930 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11931 cx.assert_editor_state(indoc! {"
11932 a.f(one, two, three)ˇ b
11933 a.f(one, two, three)ˇ b
11934 a.f(one, two, three)ˇ b
11935 "});
11936
11937 // As soon as the last tab stop is reached, snippet state is gone
11938 cx.update_editor(|editor, window, cx| {
11939 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11940 });
11941 cx.assert_editor_state(indoc! {"
11942 a.f(one, two, three)ˇ b
11943 a.f(one, two, three)ˇ b
11944 a.f(one, two, three)ˇ b
11945 "});
11946}
11947
11948#[gpui::test]
11949async fn test_snippet_indentation(cx: &mut TestAppContext) {
11950 init_test(cx, |_| {});
11951
11952 let mut cx = EditorTestContext::new(cx).await;
11953
11954 cx.update_editor(|editor, window, cx| {
11955 let snippet = Snippet::parse(indoc! {"
11956 /*
11957 * Multiline comment with leading indentation
11958 *
11959 * $1
11960 */
11961 $0"})
11962 .unwrap();
11963 let insertion_ranges = editor
11964 .selections
11965 .all(&editor.display_snapshot(cx))
11966 .iter()
11967 .map(|s| s.range())
11968 .collect::<Vec<_>>();
11969 editor
11970 .insert_snippet(&insertion_ranges, snippet, window, cx)
11971 .unwrap();
11972 });
11973
11974 cx.assert_editor_state(indoc! {"
11975 /*
11976 * Multiline comment with leading indentation
11977 *
11978 * ˇ
11979 */
11980 "});
11981
11982 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11983 cx.assert_editor_state(indoc! {"
11984 /*
11985 * Multiline comment with leading indentation
11986 *
11987 *•
11988 */
11989 ˇ"});
11990}
11991
11992#[gpui::test]
11993async fn test_snippet_with_multi_word_prefix(cx: &mut TestAppContext) {
11994 init_test(cx, |_| {});
11995
11996 let mut cx = EditorTestContext::new(cx).await;
11997 cx.update_editor(|editor, _, cx| {
11998 editor.project().unwrap().update(cx, |project, cx| {
11999 project.snippets().update(cx, |snippets, _cx| {
12000 let snippet = project::snippet_provider::Snippet {
12001 prefix: vec!["multi word".to_string()],
12002 body: "this is many words".to_string(),
12003 description: Some("description".to_string()),
12004 name: "multi-word snippet test".to_string(),
12005 };
12006 snippets.add_snippet_for_test(
12007 None,
12008 PathBuf::from("test_snippets.json"),
12009 vec![Arc::new(snippet)],
12010 );
12011 });
12012 })
12013 });
12014
12015 for (input_to_simulate, should_match_snippet) in [
12016 ("m", true),
12017 ("m ", true),
12018 ("m w", true),
12019 ("aa m w", true),
12020 ("aa m g", false),
12021 ] {
12022 cx.set_state("ˇ");
12023 cx.simulate_input(input_to_simulate); // fails correctly
12024
12025 cx.update_editor(|editor, _, _| {
12026 let Some(CodeContextMenu::Completions(context_menu)) = &*editor.context_menu.borrow()
12027 else {
12028 assert!(!should_match_snippet); // no completions! don't even show the menu
12029 return;
12030 };
12031 assert!(context_menu.visible());
12032 let completions = context_menu.completions.borrow();
12033
12034 assert_eq!(!completions.is_empty(), should_match_snippet);
12035 });
12036 }
12037}
12038
12039#[gpui::test]
12040async fn test_document_format_during_save(cx: &mut TestAppContext) {
12041 init_test(cx, |_| {});
12042
12043 let fs = FakeFs::new(cx.executor());
12044 fs.insert_file(path!("/file.rs"), Default::default()).await;
12045
12046 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
12047
12048 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12049 language_registry.add(rust_lang());
12050 let mut fake_servers = language_registry.register_fake_lsp(
12051 "Rust",
12052 FakeLspAdapter {
12053 capabilities: lsp::ServerCapabilities {
12054 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12055 ..Default::default()
12056 },
12057 ..Default::default()
12058 },
12059 );
12060
12061 let buffer = project
12062 .update(cx, |project, cx| {
12063 project.open_local_buffer(path!("/file.rs"), cx)
12064 })
12065 .await
12066 .unwrap();
12067
12068 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12069 let (editor, cx) = cx.add_window_view(|window, cx| {
12070 build_editor_with_project(project.clone(), buffer, window, cx)
12071 });
12072 editor.update_in(cx, |editor, window, cx| {
12073 editor.set_text("one\ntwo\nthree\n", window, cx)
12074 });
12075 assert!(cx.read(|cx| editor.is_dirty(cx)));
12076
12077 let fake_server = fake_servers.next().await.unwrap();
12078
12079 {
12080 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12081 move |params, _| async move {
12082 assert_eq!(
12083 params.text_document.uri,
12084 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12085 );
12086 assert_eq!(params.options.tab_size, 4);
12087 Ok(Some(vec![lsp::TextEdit::new(
12088 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12089 ", ".to_string(),
12090 )]))
12091 },
12092 );
12093 let save = editor
12094 .update_in(cx, |editor, window, cx| {
12095 editor.save(
12096 SaveOptions {
12097 format: true,
12098 autosave: false,
12099 },
12100 project.clone(),
12101 window,
12102 cx,
12103 )
12104 })
12105 .unwrap();
12106 save.await;
12107
12108 assert_eq!(
12109 editor.update(cx, |editor, cx| editor.text(cx)),
12110 "one, two\nthree\n"
12111 );
12112 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12113 }
12114
12115 {
12116 editor.update_in(cx, |editor, window, cx| {
12117 editor.set_text("one\ntwo\nthree\n", window, cx)
12118 });
12119 assert!(cx.read(|cx| editor.is_dirty(cx)));
12120
12121 // Ensure we can still save even if formatting hangs.
12122 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12123 move |params, _| async move {
12124 assert_eq!(
12125 params.text_document.uri,
12126 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12127 );
12128 futures::future::pending::<()>().await;
12129 unreachable!()
12130 },
12131 );
12132 let save = editor
12133 .update_in(cx, |editor, window, cx| {
12134 editor.save(
12135 SaveOptions {
12136 format: true,
12137 autosave: false,
12138 },
12139 project.clone(),
12140 window,
12141 cx,
12142 )
12143 })
12144 .unwrap();
12145 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12146 save.await;
12147 assert_eq!(
12148 editor.update(cx, |editor, cx| editor.text(cx)),
12149 "one\ntwo\nthree\n"
12150 );
12151 }
12152
12153 // Set rust language override and assert overridden tabsize is sent to language server
12154 update_test_language_settings(cx, |settings| {
12155 settings.languages.0.insert(
12156 "Rust".into(),
12157 LanguageSettingsContent {
12158 tab_size: NonZeroU32::new(8),
12159 ..Default::default()
12160 },
12161 );
12162 });
12163
12164 {
12165 editor.update_in(cx, |editor, window, cx| {
12166 editor.set_text("somehting_new\n", window, cx)
12167 });
12168 assert!(cx.read(|cx| editor.is_dirty(cx)));
12169 let _formatting_request_signal = fake_server
12170 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12171 assert_eq!(
12172 params.text_document.uri,
12173 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12174 );
12175 assert_eq!(params.options.tab_size, 8);
12176 Ok(Some(vec![]))
12177 });
12178 let save = editor
12179 .update_in(cx, |editor, window, cx| {
12180 editor.save(
12181 SaveOptions {
12182 format: true,
12183 autosave: false,
12184 },
12185 project.clone(),
12186 window,
12187 cx,
12188 )
12189 })
12190 .unwrap();
12191 save.await;
12192 }
12193}
12194
12195#[gpui::test]
12196async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
12197 init_test(cx, |settings| {
12198 settings.defaults.ensure_final_newline_on_save = Some(false);
12199 });
12200
12201 let fs = FakeFs::new(cx.executor());
12202 fs.insert_file(path!("/file.txt"), "foo".into()).await;
12203
12204 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
12205
12206 let buffer = project
12207 .update(cx, |project, cx| {
12208 project.open_local_buffer(path!("/file.txt"), cx)
12209 })
12210 .await
12211 .unwrap();
12212
12213 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12214 let (editor, cx) = cx.add_window_view(|window, cx| {
12215 build_editor_with_project(project.clone(), buffer, window, cx)
12216 });
12217 editor.update_in(cx, |editor, window, cx| {
12218 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
12219 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
12220 });
12221 });
12222 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12223
12224 editor.update_in(cx, |editor, window, cx| {
12225 editor.handle_input("\n", window, cx)
12226 });
12227 cx.run_until_parked();
12228 save(&editor, &project, cx).await;
12229 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
12230
12231 editor.update_in(cx, |editor, window, cx| {
12232 editor.undo(&Default::default(), window, cx);
12233 });
12234 save(&editor, &project, cx).await;
12235 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
12236
12237 editor.update_in(cx, |editor, window, cx| {
12238 editor.redo(&Default::default(), window, cx);
12239 });
12240 cx.run_until_parked();
12241 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
12242
12243 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
12244 let save = editor
12245 .update_in(cx, |editor, window, cx| {
12246 editor.save(
12247 SaveOptions {
12248 format: true,
12249 autosave: false,
12250 },
12251 project.clone(),
12252 window,
12253 cx,
12254 )
12255 })
12256 .unwrap();
12257 save.await;
12258 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12259 }
12260}
12261
12262#[gpui::test]
12263async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
12264 init_test(cx, |_| {});
12265
12266 let cols = 4;
12267 let rows = 10;
12268 let sample_text_1 = sample_text(rows, cols, 'a');
12269 assert_eq!(
12270 sample_text_1,
12271 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
12272 );
12273 let sample_text_2 = sample_text(rows, cols, 'l');
12274 assert_eq!(
12275 sample_text_2,
12276 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
12277 );
12278 let sample_text_3 = sample_text(rows, cols, 'v');
12279 assert_eq!(
12280 sample_text_3,
12281 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
12282 );
12283
12284 let fs = FakeFs::new(cx.executor());
12285 fs.insert_tree(
12286 path!("/a"),
12287 json!({
12288 "main.rs": sample_text_1,
12289 "other.rs": sample_text_2,
12290 "lib.rs": sample_text_3,
12291 }),
12292 )
12293 .await;
12294
12295 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12296 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12297 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12298
12299 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12300 language_registry.add(rust_lang());
12301 let mut fake_servers = language_registry.register_fake_lsp(
12302 "Rust",
12303 FakeLspAdapter {
12304 capabilities: lsp::ServerCapabilities {
12305 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12306 ..Default::default()
12307 },
12308 ..Default::default()
12309 },
12310 );
12311
12312 let worktree = project.update(cx, |project, cx| {
12313 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
12314 assert_eq!(worktrees.len(), 1);
12315 worktrees.pop().unwrap()
12316 });
12317 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12318
12319 let buffer_1 = project
12320 .update(cx, |project, cx| {
12321 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
12322 })
12323 .await
12324 .unwrap();
12325 let buffer_2 = project
12326 .update(cx, |project, cx| {
12327 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
12328 })
12329 .await
12330 .unwrap();
12331 let buffer_3 = project
12332 .update(cx, |project, cx| {
12333 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
12334 })
12335 .await
12336 .unwrap();
12337
12338 let multi_buffer = cx.new(|cx| {
12339 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12340 multi_buffer.push_excerpts(
12341 buffer_1.clone(),
12342 [
12343 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12344 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12345 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12346 ],
12347 cx,
12348 );
12349 multi_buffer.push_excerpts(
12350 buffer_2.clone(),
12351 [
12352 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12353 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12354 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12355 ],
12356 cx,
12357 );
12358 multi_buffer.push_excerpts(
12359 buffer_3.clone(),
12360 [
12361 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
12362 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
12363 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
12364 ],
12365 cx,
12366 );
12367 multi_buffer
12368 });
12369 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
12370 Editor::new(
12371 EditorMode::full(),
12372 multi_buffer,
12373 Some(project.clone()),
12374 window,
12375 cx,
12376 )
12377 });
12378
12379 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12380 editor.change_selections(
12381 SelectionEffects::scroll(Autoscroll::Next),
12382 window,
12383 cx,
12384 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
12385 );
12386 editor.insert("|one|two|three|", window, cx);
12387 });
12388 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
12389 multi_buffer_editor.update_in(cx, |editor, window, cx| {
12390 editor.change_selections(
12391 SelectionEffects::scroll(Autoscroll::Next),
12392 window,
12393 cx,
12394 |s| s.select_ranges(Some(MultiBufferOffset(60)..MultiBufferOffset(70))),
12395 );
12396 editor.insert("|four|five|six|", window, cx);
12397 });
12398 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
12399
12400 // First two buffers should be edited, but not the third one.
12401 assert_eq!(
12402 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
12403 "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}",
12404 );
12405 buffer_1.update(cx, |buffer, _| {
12406 assert!(buffer.is_dirty());
12407 assert_eq!(
12408 buffer.text(),
12409 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
12410 )
12411 });
12412 buffer_2.update(cx, |buffer, _| {
12413 assert!(buffer.is_dirty());
12414 assert_eq!(
12415 buffer.text(),
12416 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
12417 )
12418 });
12419 buffer_3.update(cx, |buffer, _| {
12420 assert!(!buffer.is_dirty());
12421 assert_eq!(buffer.text(), sample_text_3,)
12422 });
12423 cx.executor().run_until_parked();
12424
12425 let save = multi_buffer_editor
12426 .update_in(cx, |editor, window, cx| {
12427 editor.save(
12428 SaveOptions {
12429 format: true,
12430 autosave: false,
12431 },
12432 project.clone(),
12433 window,
12434 cx,
12435 )
12436 })
12437 .unwrap();
12438
12439 let fake_server = fake_servers.next().await.unwrap();
12440 fake_server
12441 .server
12442 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
12443 Ok(Some(vec![lsp::TextEdit::new(
12444 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12445 format!("[{} formatted]", params.text_document.uri),
12446 )]))
12447 })
12448 .detach();
12449 save.await;
12450
12451 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
12452 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
12453 assert_eq!(
12454 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
12455 uri!(
12456 "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}"
12457 ),
12458 );
12459 buffer_1.update(cx, |buffer, _| {
12460 assert!(!buffer.is_dirty());
12461 assert_eq!(
12462 buffer.text(),
12463 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
12464 )
12465 });
12466 buffer_2.update(cx, |buffer, _| {
12467 assert!(!buffer.is_dirty());
12468 assert_eq!(
12469 buffer.text(),
12470 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
12471 )
12472 });
12473 buffer_3.update(cx, |buffer, _| {
12474 assert!(!buffer.is_dirty());
12475 assert_eq!(buffer.text(), sample_text_3,)
12476 });
12477}
12478
12479#[gpui::test]
12480async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
12481 init_test(cx, |_| {});
12482
12483 let fs = FakeFs::new(cx.executor());
12484 fs.insert_tree(
12485 path!("/dir"),
12486 json!({
12487 "file1.rs": "fn main() { println!(\"hello\"); }",
12488 "file2.rs": "fn test() { println!(\"test\"); }",
12489 "file3.rs": "fn other() { println!(\"other\"); }\n",
12490 }),
12491 )
12492 .await;
12493
12494 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
12495 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12496 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12497
12498 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12499 language_registry.add(rust_lang());
12500
12501 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
12502 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
12503
12504 // Open three buffers
12505 let buffer_1 = project
12506 .update(cx, |project, cx| {
12507 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
12508 })
12509 .await
12510 .unwrap();
12511 let buffer_2 = project
12512 .update(cx, |project, cx| {
12513 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
12514 })
12515 .await
12516 .unwrap();
12517 let buffer_3 = project
12518 .update(cx, |project, cx| {
12519 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
12520 })
12521 .await
12522 .unwrap();
12523
12524 // Create a multi-buffer with all three buffers
12525 let multi_buffer = cx.new(|cx| {
12526 let mut multi_buffer = MultiBuffer::new(ReadWrite);
12527 multi_buffer.push_excerpts(
12528 buffer_1.clone(),
12529 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12530 cx,
12531 );
12532 multi_buffer.push_excerpts(
12533 buffer_2.clone(),
12534 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12535 cx,
12536 );
12537 multi_buffer.push_excerpts(
12538 buffer_3.clone(),
12539 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
12540 cx,
12541 );
12542 multi_buffer
12543 });
12544
12545 let editor = cx.new_window_entity(|window, cx| {
12546 Editor::new(
12547 EditorMode::full(),
12548 multi_buffer,
12549 Some(project.clone()),
12550 window,
12551 cx,
12552 )
12553 });
12554
12555 // Edit only the first buffer
12556 editor.update_in(cx, |editor, window, cx| {
12557 editor.change_selections(
12558 SelectionEffects::scroll(Autoscroll::Next),
12559 window,
12560 cx,
12561 |s| s.select_ranges(Some(MultiBufferOffset(10)..MultiBufferOffset(10))),
12562 );
12563 editor.insert("// edited", window, cx);
12564 });
12565
12566 // Verify that only buffer 1 is dirty
12567 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
12568 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12569 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12570
12571 // Get write counts after file creation (files were created with initial content)
12572 // We expect each file to have been written once during creation
12573 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
12574 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
12575 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
12576
12577 // Perform autosave
12578 let save_task = editor.update_in(cx, |editor, window, cx| {
12579 editor.save(
12580 SaveOptions {
12581 format: true,
12582 autosave: true,
12583 },
12584 project.clone(),
12585 window,
12586 cx,
12587 )
12588 });
12589 save_task.await.unwrap();
12590
12591 // Only the dirty buffer should have been saved
12592 assert_eq!(
12593 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12594 1,
12595 "Buffer 1 was dirty, so it should have been written once during autosave"
12596 );
12597 assert_eq!(
12598 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12599 0,
12600 "Buffer 2 was clean, so it should not have been written during autosave"
12601 );
12602 assert_eq!(
12603 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12604 0,
12605 "Buffer 3 was clean, so it should not have been written during autosave"
12606 );
12607
12608 // Verify buffer states after autosave
12609 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12610 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12611 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
12612
12613 // Now perform a manual save (format = true)
12614 let save_task = editor.update_in(cx, |editor, window, cx| {
12615 editor.save(
12616 SaveOptions {
12617 format: true,
12618 autosave: false,
12619 },
12620 project.clone(),
12621 window,
12622 cx,
12623 )
12624 });
12625 save_task.await.unwrap();
12626
12627 // During manual save, clean buffers don't get written to disk
12628 // They just get did_save called for language server notifications
12629 assert_eq!(
12630 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
12631 1,
12632 "Buffer 1 should only have been written once total (during autosave, not manual save)"
12633 );
12634 assert_eq!(
12635 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
12636 0,
12637 "Buffer 2 should not have been written at all"
12638 );
12639 assert_eq!(
12640 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
12641 0,
12642 "Buffer 3 should not have been written at all"
12643 );
12644}
12645
12646async fn setup_range_format_test(
12647 cx: &mut TestAppContext,
12648) -> (
12649 Entity<Project>,
12650 Entity<Editor>,
12651 &mut gpui::VisualTestContext,
12652 lsp::FakeLanguageServer,
12653) {
12654 init_test(cx, |_| {});
12655
12656 let fs = FakeFs::new(cx.executor());
12657 fs.insert_file(path!("/file.rs"), Default::default()).await;
12658
12659 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12660
12661 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12662 language_registry.add(rust_lang());
12663 let mut fake_servers = language_registry.register_fake_lsp(
12664 "Rust",
12665 FakeLspAdapter {
12666 capabilities: lsp::ServerCapabilities {
12667 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
12668 ..lsp::ServerCapabilities::default()
12669 },
12670 ..FakeLspAdapter::default()
12671 },
12672 );
12673
12674 let buffer = project
12675 .update(cx, |project, cx| {
12676 project.open_local_buffer(path!("/file.rs"), cx)
12677 })
12678 .await
12679 .unwrap();
12680
12681 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12682 let (editor, cx) = cx.add_window_view(|window, cx| {
12683 build_editor_with_project(project.clone(), buffer, window, cx)
12684 });
12685
12686 let fake_server = fake_servers.next().await.unwrap();
12687
12688 (project, editor, cx, fake_server)
12689}
12690
12691#[gpui::test]
12692async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
12693 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12694
12695 editor.update_in(cx, |editor, window, cx| {
12696 editor.set_text("one\ntwo\nthree\n", window, cx)
12697 });
12698 assert!(cx.read(|cx| editor.is_dirty(cx)));
12699
12700 let save = editor
12701 .update_in(cx, |editor, window, cx| {
12702 editor.save(
12703 SaveOptions {
12704 format: true,
12705 autosave: false,
12706 },
12707 project.clone(),
12708 window,
12709 cx,
12710 )
12711 })
12712 .unwrap();
12713 fake_server
12714 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12715 assert_eq!(
12716 params.text_document.uri,
12717 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12718 );
12719 assert_eq!(params.options.tab_size, 4);
12720 Ok(Some(vec![lsp::TextEdit::new(
12721 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12722 ", ".to_string(),
12723 )]))
12724 })
12725 .next()
12726 .await;
12727 save.await;
12728 assert_eq!(
12729 editor.update(cx, |editor, cx| editor.text(cx)),
12730 "one, two\nthree\n"
12731 );
12732 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12733}
12734
12735#[gpui::test]
12736async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
12737 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12738
12739 editor.update_in(cx, |editor, window, cx| {
12740 editor.set_text("one\ntwo\nthree\n", window, cx)
12741 });
12742 assert!(cx.read(|cx| editor.is_dirty(cx)));
12743
12744 // Test that save still works when formatting hangs
12745 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
12746 move |params, _| async move {
12747 assert_eq!(
12748 params.text_document.uri,
12749 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12750 );
12751 futures::future::pending::<()>().await;
12752 unreachable!()
12753 },
12754 );
12755 let save = editor
12756 .update_in(cx, |editor, window, cx| {
12757 editor.save(
12758 SaveOptions {
12759 format: true,
12760 autosave: false,
12761 },
12762 project.clone(),
12763 window,
12764 cx,
12765 )
12766 })
12767 .unwrap();
12768 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12769 save.await;
12770 assert_eq!(
12771 editor.update(cx, |editor, cx| editor.text(cx)),
12772 "one\ntwo\nthree\n"
12773 );
12774 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12775}
12776
12777#[gpui::test]
12778async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
12779 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12780
12781 // Buffer starts clean, no formatting should be requested
12782 let save = editor
12783 .update_in(cx, |editor, window, cx| {
12784 editor.save(
12785 SaveOptions {
12786 format: false,
12787 autosave: false,
12788 },
12789 project.clone(),
12790 window,
12791 cx,
12792 )
12793 })
12794 .unwrap();
12795 let _pending_format_request = fake_server
12796 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12797 panic!("Should not be invoked");
12798 })
12799 .next();
12800 save.await;
12801 cx.run_until_parked();
12802}
12803
12804#[gpui::test]
12805async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12806 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12807
12808 // Set Rust language override and assert overridden tabsize is sent to language server
12809 update_test_language_settings(cx, |settings| {
12810 settings.languages.0.insert(
12811 "Rust".into(),
12812 LanguageSettingsContent {
12813 tab_size: NonZeroU32::new(8),
12814 ..Default::default()
12815 },
12816 );
12817 });
12818
12819 editor.update_in(cx, |editor, window, cx| {
12820 editor.set_text("something_new\n", window, cx)
12821 });
12822 assert!(cx.read(|cx| editor.is_dirty(cx)));
12823 let save = editor
12824 .update_in(cx, |editor, window, cx| {
12825 editor.save(
12826 SaveOptions {
12827 format: true,
12828 autosave: false,
12829 },
12830 project.clone(),
12831 window,
12832 cx,
12833 )
12834 })
12835 .unwrap();
12836 fake_server
12837 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12838 assert_eq!(
12839 params.text_document.uri,
12840 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12841 );
12842 assert_eq!(params.options.tab_size, 8);
12843 Ok(Some(Vec::new()))
12844 })
12845 .next()
12846 .await;
12847 save.await;
12848}
12849
12850#[gpui::test]
12851async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12852 init_test(cx, |settings| {
12853 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12854 settings::LanguageServerFormatterSpecifier::Current,
12855 )))
12856 });
12857
12858 let fs = FakeFs::new(cx.executor());
12859 fs.insert_file(path!("/file.rs"), Default::default()).await;
12860
12861 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12862
12863 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12864 language_registry.add(Arc::new(Language::new(
12865 LanguageConfig {
12866 name: "Rust".into(),
12867 matcher: LanguageMatcher {
12868 path_suffixes: vec!["rs".to_string()],
12869 ..Default::default()
12870 },
12871 ..LanguageConfig::default()
12872 },
12873 Some(tree_sitter_rust::LANGUAGE.into()),
12874 )));
12875 update_test_language_settings(cx, |settings| {
12876 // Enable Prettier formatting for the same buffer, and ensure
12877 // LSP is called instead of Prettier.
12878 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12879 });
12880 let mut fake_servers = language_registry.register_fake_lsp(
12881 "Rust",
12882 FakeLspAdapter {
12883 capabilities: lsp::ServerCapabilities {
12884 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12885 ..Default::default()
12886 },
12887 ..Default::default()
12888 },
12889 );
12890
12891 let buffer = project
12892 .update(cx, |project, cx| {
12893 project.open_local_buffer(path!("/file.rs"), cx)
12894 })
12895 .await
12896 .unwrap();
12897
12898 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12899 let (editor, cx) = cx.add_window_view(|window, cx| {
12900 build_editor_with_project(project.clone(), buffer, window, cx)
12901 });
12902 editor.update_in(cx, |editor, window, cx| {
12903 editor.set_text("one\ntwo\nthree\n", window, cx)
12904 });
12905
12906 let fake_server = fake_servers.next().await.unwrap();
12907
12908 let format = editor
12909 .update_in(cx, |editor, window, cx| {
12910 editor.perform_format(
12911 project.clone(),
12912 FormatTrigger::Manual,
12913 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12914 window,
12915 cx,
12916 )
12917 })
12918 .unwrap();
12919 fake_server
12920 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12921 assert_eq!(
12922 params.text_document.uri,
12923 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12924 );
12925 assert_eq!(params.options.tab_size, 4);
12926 Ok(Some(vec![lsp::TextEdit::new(
12927 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12928 ", ".to_string(),
12929 )]))
12930 })
12931 .next()
12932 .await;
12933 format.await;
12934 assert_eq!(
12935 editor.update(cx, |editor, cx| editor.text(cx)),
12936 "one, two\nthree\n"
12937 );
12938
12939 editor.update_in(cx, |editor, window, cx| {
12940 editor.set_text("one\ntwo\nthree\n", window, cx)
12941 });
12942 // Ensure we don't lock if formatting hangs.
12943 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12944 move |params, _| async move {
12945 assert_eq!(
12946 params.text_document.uri,
12947 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12948 );
12949 futures::future::pending::<()>().await;
12950 unreachable!()
12951 },
12952 );
12953 let format = editor
12954 .update_in(cx, |editor, window, cx| {
12955 editor.perform_format(
12956 project,
12957 FormatTrigger::Manual,
12958 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12959 window,
12960 cx,
12961 )
12962 })
12963 .unwrap();
12964 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12965 format.await;
12966 assert_eq!(
12967 editor.update(cx, |editor, cx| editor.text(cx)),
12968 "one\ntwo\nthree\n"
12969 );
12970}
12971
12972#[gpui::test]
12973async fn test_multiple_formatters(cx: &mut TestAppContext) {
12974 init_test(cx, |settings| {
12975 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12976 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12977 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12978 Formatter::CodeAction("code-action-1".into()),
12979 Formatter::CodeAction("code-action-2".into()),
12980 ]))
12981 });
12982
12983 let fs = FakeFs::new(cx.executor());
12984 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12985 .await;
12986
12987 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12988 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12989 language_registry.add(rust_lang());
12990
12991 let mut fake_servers = language_registry.register_fake_lsp(
12992 "Rust",
12993 FakeLspAdapter {
12994 capabilities: lsp::ServerCapabilities {
12995 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12996 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12997 commands: vec!["the-command-for-code-action-1".into()],
12998 ..Default::default()
12999 }),
13000 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
13001 ..Default::default()
13002 },
13003 ..Default::default()
13004 },
13005 );
13006
13007 let buffer = project
13008 .update(cx, |project, cx| {
13009 project.open_local_buffer(path!("/file.rs"), cx)
13010 })
13011 .await
13012 .unwrap();
13013
13014 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13015 let (editor, cx) = cx.add_window_view(|window, cx| {
13016 build_editor_with_project(project.clone(), buffer, window, cx)
13017 });
13018
13019 let fake_server = fake_servers.next().await.unwrap();
13020 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
13021 move |_params, _| async move {
13022 Ok(Some(vec![lsp::TextEdit::new(
13023 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
13024 "applied-formatting\n".to_string(),
13025 )]))
13026 },
13027 );
13028 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
13029 move |params, _| async move {
13030 let requested_code_actions = params.context.only.expect("Expected code action request");
13031 assert_eq!(requested_code_actions.len(), 1);
13032
13033 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
13034 let code_action = match requested_code_actions[0].as_str() {
13035 "code-action-1" => lsp::CodeAction {
13036 kind: Some("code-action-1".into()),
13037 edit: Some(lsp::WorkspaceEdit::new(
13038 [(
13039 uri,
13040 vec![lsp::TextEdit::new(
13041 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
13042 "applied-code-action-1-edit\n".to_string(),
13043 )],
13044 )]
13045 .into_iter()
13046 .collect(),
13047 )),
13048 command: Some(lsp::Command {
13049 command: "the-command-for-code-action-1".into(),
13050 ..Default::default()
13051 }),
13052 ..Default::default()
13053 },
13054 "code-action-2" => lsp::CodeAction {
13055 kind: Some("code-action-2".into()),
13056 edit: Some(lsp::WorkspaceEdit::new(
13057 [(
13058 uri,
13059 vec![lsp::TextEdit::new(
13060 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
13061 "applied-code-action-2-edit\n".to_string(),
13062 )],
13063 )]
13064 .into_iter()
13065 .collect(),
13066 )),
13067 ..Default::default()
13068 },
13069 req => panic!("Unexpected code action request: {:?}", req),
13070 };
13071 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
13072 code_action,
13073 )]))
13074 },
13075 );
13076
13077 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
13078 move |params, _| async move { Ok(params) }
13079 });
13080
13081 let command_lock = Arc::new(futures::lock::Mutex::new(()));
13082 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
13083 let fake = fake_server.clone();
13084 let lock = command_lock.clone();
13085 move |params, _| {
13086 assert_eq!(params.command, "the-command-for-code-action-1");
13087 let fake = fake.clone();
13088 let lock = lock.clone();
13089 async move {
13090 lock.lock().await;
13091 fake.server
13092 .request::<lsp::request::ApplyWorkspaceEdit>(
13093 lsp::ApplyWorkspaceEditParams {
13094 label: None,
13095 edit: lsp::WorkspaceEdit {
13096 changes: Some(
13097 [(
13098 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
13099 vec![lsp::TextEdit {
13100 range: lsp::Range::new(
13101 lsp::Position::new(0, 0),
13102 lsp::Position::new(0, 0),
13103 ),
13104 new_text: "applied-code-action-1-command\n".into(),
13105 }],
13106 )]
13107 .into_iter()
13108 .collect(),
13109 ),
13110 ..Default::default()
13111 },
13112 },
13113 DEFAULT_LSP_REQUEST_TIMEOUT,
13114 )
13115 .await
13116 .into_response()
13117 .unwrap();
13118 Ok(Some(json!(null)))
13119 }
13120 }
13121 });
13122
13123 editor
13124 .update_in(cx, |editor, window, cx| {
13125 editor.perform_format(
13126 project.clone(),
13127 FormatTrigger::Manual,
13128 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
13129 window,
13130 cx,
13131 )
13132 })
13133 .unwrap()
13134 .await;
13135 editor.update(cx, |editor, cx| {
13136 assert_eq!(
13137 editor.text(cx),
13138 r#"
13139 applied-code-action-2-edit
13140 applied-code-action-1-command
13141 applied-code-action-1-edit
13142 applied-formatting
13143 one
13144 two
13145 three
13146 "#
13147 .unindent()
13148 );
13149 });
13150
13151 editor.update_in(cx, |editor, window, cx| {
13152 editor.undo(&Default::default(), window, cx);
13153 assert_eq!(editor.text(cx), "one \ntwo \nthree");
13154 });
13155
13156 // Perform a manual edit while waiting for an LSP command
13157 // that's being run as part of a formatting code action.
13158 let lock_guard = command_lock.lock().await;
13159 let format = editor
13160 .update_in(cx, |editor, window, cx| {
13161 editor.perform_format(
13162 project.clone(),
13163 FormatTrigger::Manual,
13164 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
13165 window,
13166 cx,
13167 )
13168 })
13169 .unwrap();
13170 cx.run_until_parked();
13171 editor.update(cx, |editor, cx| {
13172 assert_eq!(
13173 editor.text(cx),
13174 r#"
13175 applied-code-action-1-edit
13176 applied-formatting
13177 one
13178 two
13179 three
13180 "#
13181 .unindent()
13182 );
13183
13184 editor.buffer.update(cx, |buffer, cx| {
13185 let ix = buffer.len(cx);
13186 buffer.edit([(ix..ix, "edited\n")], None, cx);
13187 });
13188 });
13189
13190 // Allow the LSP command to proceed. Because the buffer was edited,
13191 // the second code action will not be run.
13192 drop(lock_guard);
13193 format.await;
13194 editor.update_in(cx, |editor, window, cx| {
13195 assert_eq!(
13196 editor.text(cx),
13197 r#"
13198 applied-code-action-1-command
13199 applied-code-action-1-edit
13200 applied-formatting
13201 one
13202 two
13203 three
13204 edited
13205 "#
13206 .unindent()
13207 );
13208
13209 // The manual edit is undone first, because it is the last thing the user did
13210 // (even though the command completed afterwards).
13211 editor.undo(&Default::default(), window, cx);
13212 assert_eq!(
13213 editor.text(cx),
13214 r#"
13215 applied-code-action-1-command
13216 applied-code-action-1-edit
13217 applied-formatting
13218 one
13219 two
13220 three
13221 "#
13222 .unindent()
13223 );
13224
13225 // All the formatting (including the command, which completed after the manual edit)
13226 // is undone together.
13227 editor.undo(&Default::default(), window, cx);
13228 assert_eq!(editor.text(cx), "one \ntwo \nthree");
13229 });
13230}
13231
13232#[gpui::test]
13233async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
13234 init_test(cx, |settings| {
13235 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
13236 settings::LanguageServerFormatterSpecifier::Current,
13237 )]))
13238 });
13239
13240 let fs = FakeFs::new(cx.executor());
13241 fs.insert_file(path!("/file.ts"), Default::default()).await;
13242
13243 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13244
13245 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13246 language_registry.add(Arc::new(Language::new(
13247 LanguageConfig {
13248 name: "TypeScript".into(),
13249 matcher: LanguageMatcher {
13250 path_suffixes: vec!["ts".to_string()],
13251 ..Default::default()
13252 },
13253 ..LanguageConfig::default()
13254 },
13255 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13256 )));
13257 update_test_language_settings(cx, |settings| {
13258 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
13259 });
13260 let mut fake_servers = language_registry.register_fake_lsp(
13261 "TypeScript",
13262 FakeLspAdapter {
13263 capabilities: lsp::ServerCapabilities {
13264 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
13265 ..Default::default()
13266 },
13267 ..Default::default()
13268 },
13269 );
13270
13271 let buffer = project
13272 .update(cx, |project, cx| {
13273 project.open_local_buffer(path!("/file.ts"), cx)
13274 })
13275 .await
13276 .unwrap();
13277
13278 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13279 let (editor, cx) = cx.add_window_view(|window, cx| {
13280 build_editor_with_project(project.clone(), buffer, window, cx)
13281 });
13282 editor.update_in(cx, |editor, window, cx| {
13283 editor.set_text(
13284 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
13285 window,
13286 cx,
13287 )
13288 });
13289
13290 let fake_server = fake_servers.next().await.unwrap();
13291
13292 let format = editor
13293 .update_in(cx, |editor, window, cx| {
13294 editor.perform_code_action_kind(
13295 project.clone(),
13296 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
13297 window,
13298 cx,
13299 )
13300 })
13301 .unwrap();
13302 fake_server
13303 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
13304 assert_eq!(
13305 params.text_document.uri,
13306 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
13307 );
13308 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
13309 lsp::CodeAction {
13310 title: "Organize Imports".to_string(),
13311 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
13312 edit: Some(lsp::WorkspaceEdit {
13313 changes: Some(
13314 [(
13315 params.text_document.uri.clone(),
13316 vec![lsp::TextEdit::new(
13317 lsp::Range::new(
13318 lsp::Position::new(1, 0),
13319 lsp::Position::new(2, 0),
13320 ),
13321 "".to_string(),
13322 )],
13323 )]
13324 .into_iter()
13325 .collect(),
13326 ),
13327 ..Default::default()
13328 }),
13329 ..Default::default()
13330 },
13331 )]))
13332 })
13333 .next()
13334 .await;
13335 format.await;
13336 assert_eq!(
13337 editor.update(cx, |editor, cx| editor.text(cx)),
13338 "import { a } from 'module';\n\nconst x = a;\n"
13339 );
13340
13341 editor.update_in(cx, |editor, window, cx| {
13342 editor.set_text(
13343 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
13344 window,
13345 cx,
13346 )
13347 });
13348 // Ensure we don't lock if code action hangs.
13349 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
13350 move |params, _| async move {
13351 assert_eq!(
13352 params.text_document.uri,
13353 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
13354 );
13355 futures::future::pending::<()>().await;
13356 unreachable!()
13357 },
13358 );
13359 let format = editor
13360 .update_in(cx, |editor, window, cx| {
13361 editor.perform_code_action_kind(
13362 project,
13363 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
13364 window,
13365 cx,
13366 )
13367 })
13368 .unwrap();
13369 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
13370 format.await;
13371 assert_eq!(
13372 editor.update(cx, |editor, cx| editor.text(cx)),
13373 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
13374 );
13375}
13376
13377#[gpui::test]
13378async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
13379 init_test(cx, |_| {});
13380
13381 let mut cx = EditorLspTestContext::new_rust(
13382 lsp::ServerCapabilities {
13383 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13384 ..Default::default()
13385 },
13386 cx,
13387 )
13388 .await;
13389
13390 cx.set_state(indoc! {"
13391 one.twoˇ
13392 "});
13393
13394 // The format request takes a long time. When it completes, it inserts
13395 // a newline and an indent before the `.`
13396 cx.lsp
13397 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
13398 let executor = cx.background_executor().clone();
13399 async move {
13400 executor.timer(Duration::from_millis(100)).await;
13401 Ok(Some(vec![lsp::TextEdit {
13402 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
13403 new_text: "\n ".into(),
13404 }]))
13405 }
13406 });
13407
13408 // Submit a format request.
13409 let format_1 = cx
13410 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13411 .unwrap();
13412 cx.executor().run_until_parked();
13413
13414 // Submit a second format request.
13415 let format_2 = cx
13416 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13417 .unwrap();
13418 cx.executor().run_until_parked();
13419
13420 // Wait for both format requests to complete
13421 cx.executor().advance_clock(Duration::from_millis(200));
13422 format_1.await.unwrap();
13423 format_2.await.unwrap();
13424
13425 // The formatting edits only happens once.
13426 cx.assert_editor_state(indoc! {"
13427 one
13428 .twoˇ
13429 "});
13430}
13431
13432#[gpui::test]
13433async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
13434 init_test(cx, |settings| {
13435 settings.defaults.formatter = Some(FormatterList::default())
13436 });
13437
13438 let mut cx = EditorLspTestContext::new_rust(
13439 lsp::ServerCapabilities {
13440 document_formatting_provider: Some(lsp::OneOf::Left(true)),
13441 ..Default::default()
13442 },
13443 cx,
13444 )
13445 .await;
13446
13447 // Record which buffer changes have been sent to the language server
13448 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
13449 cx.lsp
13450 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
13451 let buffer_changes = buffer_changes.clone();
13452 move |params, _| {
13453 buffer_changes.lock().extend(
13454 params
13455 .content_changes
13456 .into_iter()
13457 .map(|e| (e.range.unwrap(), e.text)),
13458 );
13459 }
13460 });
13461 // Handle formatting requests to the language server.
13462 cx.lsp
13463 .set_request_handler::<lsp::request::Formatting, _, _>({
13464 move |_, _| {
13465 // Insert blank lines between each line of the buffer.
13466 async move {
13467 // TODO: this assertion is not reliably true. Currently nothing guarantees that we deliver
13468 // DidChangedTextDocument to the LSP before sending the formatting request.
13469 // assert_eq!(
13470 // &buffer_changes.lock()[1..],
13471 // &[
13472 // (
13473 // lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
13474 // "".into()
13475 // ),
13476 // (
13477 // lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
13478 // "".into()
13479 // ),
13480 // (
13481 // lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
13482 // "\n".into()
13483 // ),
13484 // ]
13485 // );
13486
13487 Ok(Some(vec![
13488 lsp::TextEdit {
13489 range: lsp::Range::new(
13490 lsp::Position::new(1, 0),
13491 lsp::Position::new(1, 0),
13492 ),
13493 new_text: "\n".into(),
13494 },
13495 lsp::TextEdit {
13496 range: lsp::Range::new(
13497 lsp::Position::new(2, 0),
13498 lsp::Position::new(2, 0),
13499 ),
13500 new_text: "\n".into(),
13501 },
13502 ]))
13503 }
13504 }
13505 });
13506
13507 // Set up a buffer white some trailing whitespace and no trailing newline.
13508 cx.set_state(
13509 &[
13510 "one ", //
13511 "twoˇ", //
13512 "three ", //
13513 "four", //
13514 ]
13515 .join("\n"),
13516 );
13517
13518 // Submit a format request.
13519 let format = cx
13520 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
13521 .unwrap();
13522
13523 cx.run_until_parked();
13524 // After formatting the buffer, the trailing whitespace is stripped,
13525 // a newline is appended, and the edits provided by the language server
13526 // have been applied.
13527 format.await.unwrap();
13528
13529 cx.assert_editor_state(
13530 &[
13531 "one", //
13532 "", //
13533 "twoˇ", //
13534 "", //
13535 "three", //
13536 "four", //
13537 "", //
13538 ]
13539 .join("\n"),
13540 );
13541
13542 // Undoing the formatting undoes the trailing whitespace removal, the
13543 // trailing newline, and the LSP edits.
13544 cx.update_buffer(|buffer, cx| buffer.undo(cx));
13545 cx.assert_editor_state(
13546 &[
13547 "one ", //
13548 "twoˇ", //
13549 "three ", //
13550 "four", //
13551 ]
13552 .join("\n"),
13553 );
13554}
13555
13556#[gpui::test]
13557async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
13558 cx: &mut TestAppContext,
13559) {
13560 init_test(cx, |_| {});
13561
13562 cx.update(|cx| {
13563 cx.update_global::<SettingsStore, _>(|settings, cx| {
13564 settings.update_user_settings(cx, |settings| {
13565 settings.editor.auto_signature_help = Some(true);
13566 settings.editor.hover_popover_delay = Some(DelayMs(300));
13567 });
13568 });
13569 });
13570
13571 let mut cx = EditorLspTestContext::new_rust(
13572 lsp::ServerCapabilities {
13573 signature_help_provider: Some(lsp::SignatureHelpOptions {
13574 ..Default::default()
13575 }),
13576 ..Default::default()
13577 },
13578 cx,
13579 )
13580 .await;
13581
13582 let language = Language::new(
13583 LanguageConfig {
13584 name: "Rust".into(),
13585 brackets: BracketPairConfig {
13586 pairs: vec![
13587 BracketPair {
13588 start: "{".to_string(),
13589 end: "}".to_string(),
13590 close: true,
13591 surround: true,
13592 newline: true,
13593 },
13594 BracketPair {
13595 start: "(".to_string(),
13596 end: ")".to_string(),
13597 close: true,
13598 surround: true,
13599 newline: true,
13600 },
13601 BracketPair {
13602 start: "/*".to_string(),
13603 end: " */".to_string(),
13604 close: true,
13605 surround: true,
13606 newline: true,
13607 },
13608 BracketPair {
13609 start: "[".to_string(),
13610 end: "]".to_string(),
13611 close: false,
13612 surround: false,
13613 newline: true,
13614 },
13615 BracketPair {
13616 start: "\"".to_string(),
13617 end: "\"".to_string(),
13618 close: true,
13619 surround: true,
13620 newline: false,
13621 },
13622 BracketPair {
13623 start: "<".to_string(),
13624 end: ">".to_string(),
13625 close: false,
13626 surround: true,
13627 newline: true,
13628 },
13629 ],
13630 ..Default::default()
13631 },
13632 autoclose_before: "})]".to_string(),
13633 ..Default::default()
13634 },
13635 Some(tree_sitter_rust::LANGUAGE.into()),
13636 );
13637 let language = Arc::new(language);
13638
13639 cx.language_registry().add(language.clone());
13640 cx.update_buffer(|buffer, cx| {
13641 buffer.set_language(Some(language), cx);
13642 });
13643
13644 cx.set_state(
13645 &r#"
13646 fn main() {
13647 sampleˇ
13648 }
13649 "#
13650 .unindent(),
13651 );
13652
13653 cx.update_editor(|editor, window, cx| {
13654 editor.handle_input("(", window, cx);
13655 });
13656 cx.assert_editor_state(
13657 &"
13658 fn main() {
13659 sample(ˇ)
13660 }
13661 "
13662 .unindent(),
13663 );
13664
13665 let mocked_response = lsp::SignatureHelp {
13666 signatures: vec![lsp::SignatureInformation {
13667 label: "fn sample(param1: u8, param2: u8)".to_string(),
13668 documentation: None,
13669 parameters: Some(vec![
13670 lsp::ParameterInformation {
13671 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13672 documentation: None,
13673 },
13674 lsp::ParameterInformation {
13675 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13676 documentation: None,
13677 },
13678 ]),
13679 active_parameter: None,
13680 }],
13681 active_signature: Some(0),
13682 active_parameter: Some(0),
13683 };
13684 handle_signature_help_request(&mut cx, mocked_response).await;
13685
13686 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13687 .await;
13688
13689 cx.editor(|editor, _, _| {
13690 let signature_help_state = editor.signature_help_state.popover().cloned();
13691 let signature = signature_help_state.unwrap();
13692 assert_eq!(
13693 signature.signatures[signature.current_signature].label,
13694 "fn sample(param1: u8, param2: u8)"
13695 );
13696 });
13697}
13698
13699#[gpui::test]
13700async fn test_signature_help_delay_only_for_auto(cx: &mut TestAppContext) {
13701 init_test(cx, |_| {});
13702
13703 let delay_ms = 500;
13704 cx.update(|cx| {
13705 cx.update_global::<SettingsStore, _>(|settings, cx| {
13706 settings.update_user_settings(cx, |settings| {
13707 settings.editor.auto_signature_help = Some(true);
13708 settings.editor.show_signature_help_after_edits = Some(false);
13709 settings.editor.hover_popover_delay = Some(DelayMs(delay_ms));
13710 });
13711 });
13712 });
13713
13714 let mut cx = EditorLspTestContext::new_rust(
13715 lsp::ServerCapabilities {
13716 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13717 ..lsp::ServerCapabilities::default()
13718 },
13719 cx,
13720 )
13721 .await;
13722
13723 let mocked_response = lsp::SignatureHelp {
13724 signatures: vec![lsp::SignatureInformation {
13725 label: "fn sample(param1: u8)".to_string(),
13726 documentation: None,
13727 parameters: Some(vec![lsp::ParameterInformation {
13728 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13729 documentation: None,
13730 }]),
13731 active_parameter: None,
13732 }],
13733 active_signature: Some(0),
13734 active_parameter: Some(0),
13735 };
13736
13737 cx.set_state(indoc! {"
13738 fn main() {
13739 sample(ˇ);
13740 }
13741
13742 fn sample(param1: u8) {}
13743 "});
13744
13745 // Manual trigger should show immediately without delay
13746 cx.update_editor(|editor, window, cx| {
13747 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13748 });
13749 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13750 cx.run_until_parked();
13751 cx.editor(|editor, _, _| {
13752 assert!(
13753 editor.signature_help_state.is_shown(),
13754 "Manual trigger should show signature help without delay"
13755 );
13756 });
13757
13758 cx.update_editor(|editor, _, cx| {
13759 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13760 });
13761 cx.run_until_parked();
13762 cx.editor(|editor, _, _| {
13763 assert!(!editor.signature_help_state.is_shown());
13764 });
13765
13766 // Auto trigger (cursor movement into brackets) should respect delay
13767 cx.set_state(indoc! {"
13768 fn main() {
13769 sampleˇ();
13770 }
13771
13772 fn sample(param1: u8) {}
13773 "});
13774 cx.update_editor(|editor, window, cx| {
13775 editor.move_right(&MoveRight, window, cx);
13776 });
13777 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13778 cx.run_until_parked();
13779 cx.editor(|editor, _, _| {
13780 assert!(
13781 !editor.signature_help_state.is_shown(),
13782 "Auto trigger should wait for delay before showing signature help"
13783 );
13784 });
13785
13786 cx.executor()
13787 .advance_clock(Duration::from_millis(delay_ms + 50));
13788 cx.run_until_parked();
13789 cx.editor(|editor, _, _| {
13790 assert!(
13791 editor.signature_help_state.is_shown(),
13792 "Auto trigger should show signature help after delay elapsed"
13793 );
13794 });
13795}
13796
13797#[gpui::test]
13798async fn test_signature_help_after_edits_no_delay(cx: &mut TestAppContext) {
13799 init_test(cx, |_| {});
13800
13801 let delay_ms = 500;
13802 cx.update(|cx| {
13803 cx.update_global::<SettingsStore, _>(|settings, cx| {
13804 settings.update_user_settings(cx, |settings| {
13805 settings.editor.auto_signature_help = Some(false);
13806 settings.editor.show_signature_help_after_edits = Some(true);
13807 settings.editor.hover_popover_delay = Some(DelayMs(delay_ms));
13808 });
13809 });
13810 });
13811
13812 let mut cx = EditorLspTestContext::new_rust(
13813 lsp::ServerCapabilities {
13814 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13815 ..lsp::ServerCapabilities::default()
13816 },
13817 cx,
13818 )
13819 .await;
13820
13821 let language = Arc::new(Language::new(
13822 LanguageConfig {
13823 name: "Rust".into(),
13824 brackets: BracketPairConfig {
13825 pairs: vec![BracketPair {
13826 start: "(".to_string(),
13827 end: ")".to_string(),
13828 close: true,
13829 surround: true,
13830 newline: true,
13831 }],
13832 ..BracketPairConfig::default()
13833 },
13834 autoclose_before: "})".to_string(),
13835 ..LanguageConfig::default()
13836 },
13837 Some(tree_sitter_rust::LANGUAGE.into()),
13838 ));
13839 cx.language_registry().add(language.clone());
13840 cx.update_buffer(|buffer, cx| {
13841 buffer.set_language(Some(language), cx);
13842 });
13843
13844 let mocked_response = lsp::SignatureHelp {
13845 signatures: vec![lsp::SignatureInformation {
13846 label: "fn sample(param1: u8)".to_string(),
13847 documentation: None,
13848 parameters: Some(vec![lsp::ParameterInformation {
13849 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13850 documentation: None,
13851 }]),
13852 active_parameter: None,
13853 }],
13854 active_signature: Some(0),
13855 active_parameter: Some(0),
13856 };
13857
13858 cx.set_state(indoc! {"
13859 fn main() {
13860 sampleˇ
13861 }
13862 "});
13863
13864 // Typing bracket should show signature help immediately without delay
13865 cx.update_editor(|editor, window, cx| {
13866 editor.handle_input("(", window, cx);
13867 });
13868 handle_signature_help_request(&mut cx, mocked_response).await;
13869 cx.run_until_parked();
13870 cx.editor(|editor, _, _| {
13871 assert!(
13872 editor.signature_help_state.is_shown(),
13873 "show_signature_help_after_edits should show signature help without delay"
13874 );
13875 });
13876}
13877
13878#[gpui::test]
13879async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
13880 init_test(cx, |_| {});
13881
13882 cx.update(|cx| {
13883 cx.update_global::<SettingsStore, _>(|settings, cx| {
13884 settings.update_user_settings(cx, |settings| {
13885 settings.editor.auto_signature_help = Some(false);
13886 settings.editor.show_signature_help_after_edits = Some(false);
13887 });
13888 });
13889 });
13890
13891 let mut cx = EditorLspTestContext::new_rust(
13892 lsp::ServerCapabilities {
13893 signature_help_provider: Some(lsp::SignatureHelpOptions {
13894 ..Default::default()
13895 }),
13896 ..Default::default()
13897 },
13898 cx,
13899 )
13900 .await;
13901
13902 let language = Language::new(
13903 LanguageConfig {
13904 name: "Rust".into(),
13905 brackets: BracketPairConfig {
13906 pairs: vec![
13907 BracketPair {
13908 start: "{".to_string(),
13909 end: "}".to_string(),
13910 close: true,
13911 surround: true,
13912 newline: true,
13913 },
13914 BracketPair {
13915 start: "(".to_string(),
13916 end: ")".to_string(),
13917 close: true,
13918 surround: true,
13919 newline: true,
13920 },
13921 BracketPair {
13922 start: "/*".to_string(),
13923 end: " */".to_string(),
13924 close: true,
13925 surround: true,
13926 newline: true,
13927 },
13928 BracketPair {
13929 start: "[".to_string(),
13930 end: "]".to_string(),
13931 close: false,
13932 surround: false,
13933 newline: true,
13934 },
13935 BracketPair {
13936 start: "\"".to_string(),
13937 end: "\"".to_string(),
13938 close: true,
13939 surround: true,
13940 newline: false,
13941 },
13942 BracketPair {
13943 start: "<".to_string(),
13944 end: ">".to_string(),
13945 close: false,
13946 surround: true,
13947 newline: true,
13948 },
13949 ],
13950 ..Default::default()
13951 },
13952 autoclose_before: "})]".to_string(),
13953 ..Default::default()
13954 },
13955 Some(tree_sitter_rust::LANGUAGE.into()),
13956 );
13957 let language = Arc::new(language);
13958
13959 cx.language_registry().add(language.clone());
13960 cx.update_buffer(|buffer, cx| {
13961 buffer.set_language(Some(language), cx);
13962 });
13963
13964 // Ensure that signature_help is not called when no signature help is enabled.
13965 cx.set_state(
13966 &r#"
13967 fn main() {
13968 sampleˇ
13969 }
13970 "#
13971 .unindent(),
13972 );
13973 cx.update_editor(|editor, window, cx| {
13974 editor.handle_input("(", window, cx);
13975 });
13976 cx.assert_editor_state(
13977 &"
13978 fn main() {
13979 sample(ˇ)
13980 }
13981 "
13982 .unindent(),
13983 );
13984 cx.editor(|editor, _, _| {
13985 assert!(editor.signature_help_state.task().is_none());
13986 });
13987
13988 let mocked_response = lsp::SignatureHelp {
13989 signatures: vec![lsp::SignatureInformation {
13990 label: "fn sample(param1: u8, param2: u8)".to_string(),
13991 documentation: None,
13992 parameters: Some(vec![
13993 lsp::ParameterInformation {
13994 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13995 documentation: None,
13996 },
13997 lsp::ParameterInformation {
13998 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13999 documentation: None,
14000 },
14001 ]),
14002 active_parameter: None,
14003 }],
14004 active_signature: Some(0),
14005 active_parameter: Some(0),
14006 };
14007
14008 // Ensure that signature_help is called when enabled afte edits
14009 cx.update(|_, cx| {
14010 cx.update_global::<SettingsStore, _>(|settings, cx| {
14011 settings.update_user_settings(cx, |settings| {
14012 settings.editor.auto_signature_help = Some(false);
14013 settings.editor.show_signature_help_after_edits = Some(true);
14014 });
14015 });
14016 });
14017 cx.set_state(
14018 &r#"
14019 fn main() {
14020 sampleˇ
14021 }
14022 "#
14023 .unindent(),
14024 );
14025 cx.update_editor(|editor, window, cx| {
14026 editor.handle_input("(", window, cx);
14027 });
14028 cx.assert_editor_state(
14029 &"
14030 fn main() {
14031 sample(ˇ)
14032 }
14033 "
14034 .unindent(),
14035 );
14036 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14037 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14038 .await;
14039 cx.update_editor(|editor, _, _| {
14040 let signature_help_state = editor.signature_help_state.popover().cloned();
14041 assert!(signature_help_state.is_some());
14042 let signature = signature_help_state.unwrap();
14043 assert_eq!(
14044 signature.signatures[signature.current_signature].label,
14045 "fn sample(param1: u8, param2: u8)"
14046 );
14047 editor.signature_help_state = SignatureHelpState::default();
14048 });
14049
14050 // Ensure that signature_help is called when auto signature help override is enabled
14051 cx.update(|_, cx| {
14052 cx.update_global::<SettingsStore, _>(|settings, cx| {
14053 settings.update_user_settings(cx, |settings| {
14054 settings.editor.auto_signature_help = Some(true);
14055 settings.editor.show_signature_help_after_edits = Some(false);
14056 });
14057 });
14058 });
14059 cx.set_state(
14060 &r#"
14061 fn main() {
14062 sampleˇ
14063 }
14064 "#
14065 .unindent(),
14066 );
14067 cx.update_editor(|editor, window, cx| {
14068 editor.handle_input("(", window, cx);
14069 });
14070 cx.assert_editor_state(
14071 &"
14072 fn main() {
14073 sample(ˇ)
14074 }
14075 "
14076 .unindent(),
14077 );
14078 handle_signature_help_request(&mut cx, mocked_response).await;
14079 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14080 .await;
14081 cx.editor(|editor, _, _| {
14082 let signature_help_state = editor.signature_help_state.popover().cloned();
14083 assert!(signature_help_state.is_some());
14084 let signature = signature_help_state.unwrap();
14085 assert_eq!(
14086 signature.signatures[signature.current_signature].label,
14087 "fn sample(param1: u8, param2: u8)"
14088 );
14089 });
14090}
14091
14092#[gpui::test]
14093async fn test_signature_help(cx: &mut TestAppContext) {
14094 init_test(cx, |_| {});
14095 cx.update(|cx| {
14096 cx.update_global::<SettingsStore, _>(|settings, cx| {
14097 settings.update_user_settings(cx, |settings| {
14098 settings.editor.auto_signature_help = Some(true);
14099 });
14100 });
14101 });
14102
14103 let mut cx = EditorLspTestContext::new_rust(
14104 lsp::ServerCapabilities {
14105 signature_help_provider: Some(lsp::SignatureHelpOptions {
14106 ..Default::default()
14107 }),
14108 ..Default::default()
14109 },
14110 cx,
14111 )
14112 .await;
14113
14114 // A test that directly calls `show_signature_help`
14115 cx.update_editor(|editor, window, cx| {
14116 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14117 });
14118
14119 let mocked_response = lsp::SignatureHelp {
14120 signatures: vec![lsp::SignatureInformation {
14121 label: "fn sample(param1: u8, param2: u8)".to_string(),
14122 documentation: None,
14123 parameters: Some(vec![
14124 lsp::ParameterInformation {
14125 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14126 documentation: None,
14127 },
14128 lsp::ParameterInformation {
14129 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14130 documentation: None,
14131 },
14132 ]),
14133 active_parameter: None,
14134 }],
14135 active_signature: Some(0),
14136 active_parameter: Some(0),
14137 };
14138 handle_signature_help_request(&mut cx, mocked_response).await;
14139
14140 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14141 .await;
14142
14143 cx.editor(|editor, _, _| {
14144 let signature_help_state = editor.signature_help_state.popover().cloned();
14145 assert!(signature_help_state.is_some());
14146 let signature = signature_help_state.unwrap();
14147 assert_eq!(
14148 signature.signatures[signature.current_signature].label,
14149 "fn sample(param1: u8, param2: u8)"
14150 );
14151 });
14152
14153 // When exiting outside from inside the brackets, `signature_help` is closed.
14154 cx.set_state(indoc! {"
14155 fn main() {
14156 sample(ˇ);
14157 }
14158
14159 fn sample(param1: u8, param2: u8) {}
14160 "});
14161
14162 cx.update_editor(|editor, window, cx| {
14163 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14164 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
14165 });
14166 });
14167
14168 let mocked_response = lsp::SignatureHelp {
14169 signatures: Vec::new(),
14170 active_signature: None,
14171 active_parameter: None,
14172 };
14173 handle_signature_help_request(&mut cx, mocked_response).await;
14174
14175 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
14176 .await;
14177
14178 cx.editor(|editor, _, _| {
14179 assert!(!editor.signature_help_state.is_shown());
14180 });
14181
14182 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
14183 cx.set_state(indoc! {"
14184 fn main() {
14185 sample(ˇ);
14186 }
14187
14188 fn sample(param1: u8, param2: u8) {}
14189 "});
14190
14191 let mocked_response = lsp::SignatureHelp {
14192 signatures: vec![lsp::SignatureInformation {
14193 label: "fn sample(param1: u8, param2: u8)".to_string(),
14194 documentation: None,
14195 parameters: Some(vec![
14196 lsp::ParameterInformation {
14197 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14198 documentation: None,
14199 },
14200 lsp::ParameterInformation {
14201 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14202 documentation: None,
14203 },
14204 ]),
14205 active_parameter: None,
14206 }],
14207 active_signature: Some(0),
14208 active_parameter: Some(0),
14209 };
14210 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14211 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14212 .await;
14213 cx.editor(|editor, _, _| {
14214 assert!(editor.signature_help_state.is_shown());
14215 });
14216
14217 // Restore the popover with more parameter input
14218 cx.set_state(indoc! {"
14219 fn main() {
14220 sample(param1, param2ˇ);
14221 }
14222
14223 fn sample(param1: u8, param2: u8) {}
14224 "});
14225
14226 let mocked_response = lsp::SignatureHelp {
14227 signatures: vec![lsp::SignatureInformation {
14228 label: "fn sample(param1: u8, param2: u8)".to_string(),
14229 documentation: None,
14230 parameters: Some(vec![
14231 lsp::ParameterInformation {
14232 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14233 documentation: None,
14234 },
14235 lsp::ParameterInformation {
14236 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14237 documentation: None,
14238 },
14239 ]),
14240 active_parameter: None,
14241 }],
14242 active_signature: Some(0),
14243 active_parameter: Some(1),
14244 };
14245 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14246 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14247 .await;
14248
14249 // When selecting a range, the popover is gone.
14250 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
14251 cx.update_editor(|editor, window, cx| {
14252 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14253 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
14254 })
14255 });
14256 cx.assert_editor_state(indoc! {"
14257 fn main() {
14258 sample(param1, «ˇparam2»);
14259 }
14260
14261 fn sample(param1: u8, param2: u8) {}
14262 "});
14263 cx.editor(|editor, _, _| {
14264 assert!(!editor.signature_help_state.is_shown());
14265 });
14266
14267 // When unselecting again, the popover is back if within the brackets.
14268 cx.update_editor(|editor, window, cx| {
14269 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14270 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
14271 })
14272 });
14273 cx.assert_editor_state(indoc! {"
14274 fn main() {
14275 sample(param1, ˇparam2);
14276 }
14277
14278 fn sample(param1: u8, param2: u8) {}
14279 "});
14280 handle_signature_help_request(&mut cx, mocked_response).await;
14281 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14282 .await;
14283 cx.editor(|editor, _, _| {
14284 assert!(editor.signature_help_state.is_shown());
14285 });
14286
14287 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
14288 cx.update_editor(|editor, window, cx| {
14289 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14290 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
14291 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
14292 })
14293 });
14294 cx.assert_editor_state(indoc! {"
14295 fn main() {
14296 sample(param1, ˇparam2);
14297 }
14298
14299 fn sample(param1: u8, param2: u8) {}
14300 "});
14301
14302 let mocked_response = lsp::SignatureHelp {
14303 signatures: vec![lsp::SignatureInformation {
14304 label: "fn sample(param1: u8, param2: u8)".to_string(),
14305 documentation: None,
14306 parameters: Some(vec![
14307 lsp::ParameterInformation {
14308 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
14309 documentation: None,
14310 },
14311 lsp::ParameterInformation {
14312 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
14313 documentation: None,
14314 },
14315 ]),
14316 active_parameter: None,
14317 }],
14318 active_signature: Some(0),
14319 active_parameter: Some(1),
14320 };
14321 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
14322 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14323 .await;
14324 cx.update_editor(|editor, _, cx| {
14325 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
14326 });
14327 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
14328 .await;
14329 cx.update_editor(|editor, window, cx| {
14330 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14331 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
14332 })
14333 });
14334 cx.assert_editor_state(indoc! {"
14335 fn main() {
14336 sample(param1, «ˇparam2»);
14337 }
14338
14339 fn sample(param1: u8, param2: u8) {}
14340 "});
14341 cx.update_editor(|editor, window, cx| {
14342 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14343 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
14344 })
14345 });
14346 cx.assert_editor_state(indoc! {"
14347 fn main() {
14348 sample(param1, ˇparam2);
14349 }
14350
14351 fn sample(param1: u8, param2: u8) {}
14352 "});
14353 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
14354 .await;
14355}
14356
14357#[gpui::test]
14358async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
14359 init_test(cx, |_| {});
14360
14361 let mut cx = EditorLspTestContext::new_rust(
14362 lsp::ServerCapabilities {
14363 signature_help_provider: Some(lsp::SignatureHelpOptions {
14364 ..Default::default()
14365 }),
14366 ..Default::default()
14367 },
14368 cx,
14369 )
14370 .await;
14371
14372 cx.set_state(indoc! {"
14373 fn main() {
14374 overloadedˇ
14375 }
14376 "});
14377
14378 cx.update_editor(|editor, window, cx| {
14379 editor.handle_input("(", window, cx);
14380 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14381 });
14382
14383 // Mock response with 3 signatures
14384 let mocked_response = lsp::SignatureHelp {
14385 signatures: vec![
14386 lsp::SignatureInformation {
14387 label: "fn overloaded(x: i32)".to_string(),
14388 documentation: None,
14389 parameters: Some(vec![lsp::ParameterInformation {
14390 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
14391 documentation: None,
14392 }]),
14393 active_parameter: None,
14394 },
14395 lsp::SignatureInformation {
14396 label: "fn overloaded(x: i32, y: i32)".to_string(),
14397 documentation: None,
14398 parameters: Some(vec![
14399 lsp::ParameterInformation {
14400 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
14401 documentation: None,
14402 },
14403 lsp::ParameterInformation {
14404 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
14405 documentation: None,
14406 },
14407 ]),
14408 active_parameter: None,
14409 },
14410 lsp::SignatureInformation {
14411 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
14412 documentation: None,
14413 parameters: Some(vec![
14414 lsp::ParameterInformation {
14415 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
14416 documentation: None,
14417 },
14418 lsp::ParameterInformation {
14419 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
14420 documentation: None,
14421 },
14422 lsp::ParameterInformation {
14423 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
14424 documentation: None,
14425 },
14426 ]),
14427 active_parameter: None,
14428 },
14429 ],
14430 active_signature: Some(1),
14431 active_parameter: Some(0),
14432 };
14433 handle_signature_help_request(&mut cx, mocked_response).await;
14434
14435 cx.condition(|editor, _| editor.signature_help_state.is_shown())
14436 .await;
14437
14438 // Verify we have multiple signatures and the right one is selected
14439 cx.editor(|editor, _, _| {
14440 let popover = editor.signature_help_state.popover().cloned().unwrap();
14441 assert_eq!(popover.signatures.len(), 3);
14442 // active_signature was 1, so that should be the current
14443 assert_eq!(popover.current_signature, 1);
14444 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
14445 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
14446 assert_eq!(
14447 popover.signatures[2].label,
14448 "fn overloaded(x: i32, y: i32, z: i32)"
14449 );
14450 });
14451
14452 // Test navigation functionality
14453 cx.update_editor(|editor, window, cx| {
14454 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
14455 });
14456
14457 cx.editor(|editor, _, _| {
14458 let popover = editor.signature_help_state.popover().cloned().unwrap();
14459 assert_eq!(popover.current_signature, 2);
14460 });
14461
14462 // Test wrap around
14463 cx.update_editor(|editor, window, cx| {
14464 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
14465 });
14466
14467 cx.editor(|editor, _, _| {
14468 let popover = editor.signature_help_state.popover().cloned().unwrap();
14469 assert_eq!(popover.current_signature, 0);
14470 });
14471
14472 // Test previous navigation
14473 cx.update_editor(|editor, window, cx| {
14474 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
14475 });
14476
14477 cx.editor(|editor, _, _| {
14478 let popover = editor.signature_help_state.popover().cloned().unwrap();
14479 assert_eq!(popover.current_signature, 2);
14480 });
14481}
14482
14483#[gpui::test]
14484async fn test_completion_mode(cx: &mut TestAppContext) {
14485 init_test(cx, |_| {});
14486 let mut cx = EditorLspTestContext::new_rust(
14487 lsp::ServerCapabilities {
14488 completion_provider: Some(lsp::CompletionOptions {
14489 resolve_provider: Some(true),
14490 ..Default::default()
14491 }),
14492 ..Default::default()
14493 },
14494 cx,
14495 )
14496 .await;
14497
14498 struct Run {
14499 run_description: &'static str,
14500 initial_state: String,
14501 buffer_marked_text: String,
14502 completion_label: &'static str,
14503 completion_text: &'static str,
14504 expected_with_insert_mode: String,
14505 expected_with_replace_mode: String,
14506 expected_with_replace_subsequence_mode: String,
14507 expected_with_replace_suffix_mode: String,
14508 }
14509
14510 let runs = [
14511 Run {
14512 run_description: "Start of word matches completion text",
14513 initial_state: "before ediˇ after".into(),
14514 buffer_marked_text: "before <edi|> after".into(),
14515 completion_label: "editor",
14516 completion_text: "editor",
14517 expected_with_insert_mode: "before editorˇ after".into(),
14518 expected_with_replace_mode: "before editorˇ after".into(),
14519 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14520 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14521 },
14522 Run {
14523 run_description: "Accept same text at the middle of the word",
14524 initial_state: "before ediˇtor after".into(),
14525 buffer_marked_text: "before <edi|tor> after".into(),
14526 completion_label: "editor",
14527 completion_text: "editor",
14528 expected_with_insert_mode: "before editorˇtor after".into(),
14529 expected_with_replace_mode: "before editorˇ after".into(),
14530 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14531 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14532 },
14533 Run {
14534 run_description: "End of word matches completion text -- cursor at end",
14535 initial_state: "before torˇ after".into(),
14536 buffer_marked_text: "before <tor|> after".into(),
14537 completion_label: "editor",
14538 completion_text: "editor",
14539 expected_with_insert_mode: "before editorˇ after".into(),
14540 expected_with_replace_mode: "before editorˇ after".into(),
14541 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14542 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14543 },
14544 Run {
14545 run_description: "End of word matches completion text -- cursor at start",
14546 initial_state: "before ˇtor after".into(),
14547 buffer_marked_text: "before <|tor> after".into(),
14548 completion_label: "editor",
14549 completion_text: "editor",
14550 expected_with_insert_mode: "before editorˇtor after".into(),
14551 expected_with_replace_mode: "before editorˇ after".into(),
14552 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14553 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14554 },
14555 Run {
14556 run_description: "Prepend text containing whitespace",
14557 initial_state: "pˇfield: bool".into(),
14558 buffer_marked_text: "<p|field>: bool".into(),
14559 completion_label: "pub ",
14560 completion_text: "pub ",
14561 expected_with_insert_mode: "pub ˇfield: bool".into(),
14562 expected_with_replace_mode: "pub ˇ: bool".into(),
14563 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
14564 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
14565 },
14566 Run {
14567 run_description: "Add element to start of list",
14568 initial_state: "[element_ˇelement_2]".into(),
14569 buffer_marked_text: "[<element_|element_2>]".into(),
14570 completion_label: "element_1",
14571 completion_text: "element_1",
14572 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
14573 expected_with_replace_mode: "[element_1ˇ]".into(),
14574 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
14575 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
14576 },
14577 Run {
14578 run_description: "Add element to start of list -- first and second elements are equal",
14579 initial_state: "[elˇelement]".into(),
14580 buffer_marked_text: "[<el|element>]".into(),
14581 completion_label: "element",
14582 completion_text: "element",
14583 expected_with_insert_mode: "[elementˇelement]".into(),
14584 expected_with_replace_mode: "[elementˇ]".into(),
14585 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
14586 expected_with_replace_suffix_mode: "[elementˇ]".into(),
14587 },
14588 Run {
14589 run_description: "Ends with matching suffix",
14590 initial_state: "SubˇError".into(),
14591 buffer_marked_text: "<Sub|Error>".into(),
14592 completion_label: "SubscriptionError",
14593 completion_text: "SubscriptionError",
14594 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
14595 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14596 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14597 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
14598 },
14599 Run {
14600 run_description: "Suffix is a subsequence -- contiguous",
14601 initial_state: "SubˇErr".into(),
14602 buffer_marked_text: "<Sub|Err>".into(),
14603 completion_label: "SubscriptionError",
14604 completion_text: "SubscriptionError",
14605 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
14606 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14607 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14608 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
14609 },
14610 Run {
14611 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
14612 initial_state: "Suˇscrirr".into(),
14613 buffer_marked_text: "<Su|scrirr>".into(),
14614 completion_label: "SubscriptionError",
14615 completion_text: "SubscriptionError",
14616 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
14617 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
14618 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
14619 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
14620 },
14621 Run {
14622 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
14623 initial_state: "foo(indˇix)".into(),
14624 buffer_marked_text: "foo(<ind|ix>)".into(),
14625 completion_label: "node_index",
14626 completion_text: "node_index",
14627 expected_with_insert_mode: "foo(node_indexˇix)".into(),
14628 expected_with_replace_mode: "foo(node_indexˇ)".into(),
14629 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
14630 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
14631 },
14632 Run {
14633 run_description: "Replace range ends before cursor - should extend to cursor",
14634 initial_state: "before editˇo after".into(),
14635 buffer_marked_text: "before <{ed}>it|o after".into(),
14636 completion_label: "editor",
14637 completion_text: "editor",
14638 expected_with_insert_mode: "before editorˇo after".into(),
14639 expected_with_replace_mode: "before editorˇo after".into(),
14640 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
14641 expected_with_replace_suffix_mode: "before editorˇo after".into(),
14642 },
14643 Run {
14644 run_description: "Uses label for suffix matching",
14645 initial_state: "before ediˇtor after".into(),
14646 buffer_marked_text: "before <edi|tor> after".into(),
14647 completion_label: "editor",
14648 completion_text: "editor()",
14649 expected_with_insert_mode: "before editor()ˇtor after".into(),
14650 expected_with_replace_mode: "before editor()ˇ after".into(),
14651 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
14652 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
14653 },
14654 Run {
14655 run_description: "Case insensitive subsequence and suffix matching",
14656 initial_state: "before EDiˇtoR after".into(),
14657 buffer_marked_text: "before <EDi|toR> after".into(),
14658 completion_label: "editor",
14659 completion_text: "editor",
14660 expected_with_insert_mode: "before editorˇtoR after".into(),
14661 expected_with_replace_mode: "before editorˇ after".into(),
14662 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
14663 expected_with_replace_suffix_mode: "before editorˇ after".into(),
14664 },
14665 ];
14666
14667 for run in runs {
14668 let run_variations = [
14669 (LspInsertMode::Insert, run.expected_with_insert_mode),
14670 (LspInsertMode::Replace, run.expected_with_replace_mode),
14671 (
14672 LspInsertMode::ReplaceSubsequence,
14673 run.expected_with_replace_subsequence_mode,
14674 ),
14675 (
14676 LspInsertMode::ReplaceSuffix,
14677 run.expected_with_replace_suffix_mode,
14678 ),
14679 ];
14680
14681 for (lsp_insert_mode, expected_text) in run_variations {
14682 eprintln!(
14683 "run = {:?}, mode = {lsp_insert_mode:.?}",
14684 run.run_description,
14685 );
14686
14687 update_test_language_settings(&mut cx, |settings| {
14688 settings.defaults.completions = Some(CompletionSettingsContent {
14689 lsp_insert_mode: Some(lsp_insert_mode),
14690 words: Some(WordsCompletionMode::Disabled),
14691 words_min_length: Some(0),
14692 ..Default::default()
14693 });
14694 });
14695
14696 cx.set_state(&run.initial_state);
14697
14698 // Set up resolve handler before showing completions, since resolve may be
14699 // triggered when menu becomes visible (for documentation), not just on confirm.
14700 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(
14701 move |_, _, _| async move {
14702 Ok(lsp::CompletionItem {
14703 additional_text_edits: None,
14704 ..Default::default()
14705 })
14706 },
14707 );
14708
14709 cx.update_editor(|editor, window, cx| {
14710 editor.show_completions(&ShowCompletions, window, cx);
14711 });
14712
14713 let counter = Arc::new(AtomicUsize::new(0));
14714 handle_completion_request_with_insert_and_replace(
14715 &mut cx,
14716 &run.buffer_marked_text,
14717 vec![(run.completion_label, run.completion_text)],
14718 counter.clone(),
14719 )
14720 .await;
14721 cx.condition(|editor, _| editor.context_menu_visible())
14722 .await;
14723 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14724
14725 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14726 editor
14727 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14728 .unwrap()
14729 });
14730 cx.assert_editor_state(&expected_text);
14731 apply_additional_edits.await.unwrap();
14732 }
14733 }
14734}
14735
14736#[gpui::test]
14737async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
14738 init_test(cx, |_| {});
14739 let mut cx = EditorLspTestContext::new_rust(
14740 lsp::ServerCapabilities {
14741 completion_provider: Some(lsp::CompletionOptions {
14742 resolve_provider: Some(true),
14743 ..Default::default()
14744 }),
14745 ..Default::default()
14746 },
14747 cx,
14748 )
14749 .await;
14750
14751 let initial_state = "SubˇError";
14752 let buffer_marked_text = "<Sub|Error>";
14753 let completion_text = "SubscriptionError";
14754 let expected_with_insert_mode = "SubscriptionErrorˇError";
14755 let expected_with_replace_mode = "SubscriptionErrorˇ";
14756
14757 update_test_language_settings(&mut cx, |settings| {
14758 settings.defaults.completions = Some(CompletionSettingsContent {
14759 words: Some(WordsCompletionMode::Disabled),
14760 words_min_length: Some(0),
14761 // set the opposite here to ensure that the action is overriding the default behavior
14762 lsp_insert_mode: Some(LspInsertMode::Insert),
14763 ..Default::default()
14764 });
14765 });
14766
14767 cx.set_state(initial_state);
14768 cx.update_editor(|editor, window, cx| {
14769 editor.show_completions(&ShowCompletions, window, cx);
14770 });
14771
14772 let counter = Arc::new(AtomicUsize::new(0));
14773 handle_completion_request_with_insert_and_replace(
14774 &mut cx,
14775 buffer_marked_text,
14776 vec![(completion_text, completion_text)],
14777 counter.clone(),
14778 )
14779 .await;
14780 cx.condition(|editor, _| editor.context_menu_visible())
14781 .await;
14782 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14783
14784 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14785 editor
14786 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14787 .unwrap()
14788 });
14789 cx.assert_editor_state(expected_with_replace_mode);
14790 handle_resolve_completion_request(&mut cx, None).await;
14791 apply_additional_edits.await.unwrap();
14792
14793 update_test_language_settings(&mut cx, |settings| {
14794 settings.defaults.completions = Some(CompletionSettingsContent {
14795 words: Some(WordsCompletionMode::Disabled),
14796 words_min_length: Some(0),
14797 // set the opposite here to ensure that the action is overriding the default behavior
14798 lsp_insert_mode: Some(LspInsertMode::Replace),
14799 ..Default::default()
14800 });
14801 });
14802
14803 cx.set_state(initial_state);
14804 cx.update_editor(|editor, window, cx| {
14805 editor.show_completions(&ShowCompletions, window, cx);
14806 });
14807 handle_completion_request_with_insert_and_replace(
14808 &mut cx,
14809 buffer_marked_text,
14810 vec![(completion_text, completion_text)],
14811 counter.clone(),
14812 )
14813 .await;
14814 cx.condition(|editor, _| editor.context_menu_visible())
14815 .await;
14816 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14817
14818 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14819 editor
14820 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
14821 .unwrap()
14822 });
14823 cx.assert_editor_state(expected_with_insert_mode);
14824 handle_resolve_completion_request(&mut cx, None).await;
14825 apply_additional_edits.await.unwrap();
14826}
14827
14828#[gpui::test]
14829async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
14830 init_test(cx, |_| {});
14831 let mut cx = EditorLspTestContext::new_rust(
14832 lsp::ServerCapabilities {
14833 completion_provider: Some(lsp::CompletionOptions {
14834 resolve_provider: Some(true),
14835 ..Default::default()
14836 }),
14837 ..Default::default()
14838 },
14839 cx,
14840 )
14841 .await;
14842
14843 // scenario: surrounding text matches completion text
14844 let completion_text = "to_offset";
14845 let initial_state = indoc! {"
14846 1. buf.to_offˇsuffix
14847 2. buf.to_offˇsuf
14848 3. buf.to_offˇfix
14849 4. buf.to_offˇ
14850 5. into_offˇensive
14851 6. ˇsuffix
14852 7. let ˇ //
14853 8. aaˇzz
14854 9. buf.to_off«zzzzzˇ»suffix
14855 10. buf.«ˇzzzzz»suffix
14856 11. to_off«ˇzzzzz»
14857
14858 buf.to_offˇsuffix // newest cursor
14859 "};
14860 let completion_marked_buffer = indoc! {"
14861 1. buf.to_offsuffix
14862 2. buf.to_offsuf
14863 3. buf.to_offfix
14864 4. buf.to_off
14865 5. into_offensive
14866 6. suffix
14867 7. let //
14868 8. aazz
14869 9. buf.to_offzzzzzsuffix
14870 10. buf.zzzzzsuffix
14871 11. to_offzzzzz
14872
14873 buf.<to_off|suffix> // newest cursor
14874 "};
14875 let expected = indoc! {"
14876 1. buf.to_offsetˇ
14877 2. buf.to_offsetˇsuf
14878 3. buf.to_offsetˇfix
14879 4. buf.to_offsetˇ
14880 5. into_offsetˇensive
14881 6. to_offsetˇsuffix
14882 7. let to_offsetˇ //
14883 8. aato_offsetˇzz
14884 9. buf.to_offsetˇ
14885 10. buf.to_offsetˇsuffix
14886 11. to_offsetˇ
14887
14888 buf.to_offsetˇ // newest cursor
14889 "};
14890 cx.set_state(initial_state);
14891 cx.update_editor(|editor, window, cx| {
14892 editor.show_completions(&ShowCompletions, window, cx);
14893 });
14894 handle_completion_request_with_insert_and_replace(
14895 &mut cx,
14896 completion_marked_buffer,
14897 vec![(completion_text, completion_text)],
14898 Arc::new(AtomicUsize::new(0)),
14899 )
14900 .await;
14901 cx.condition(|editor, _| editor.context_menu_visible())
14902 .await;
14903 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14904 editor
14905 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14906 .unwrap()
14907 });
14908 cx.assert_editor_state(expected);
14909 handle_resolve_completion_request(&mut cx, None).await;
14910 apply_additional_edits.await.unwrap();
14911
14912 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14913 let completion_text = "foo_and_bar";
14914 let initial_state = indoc! {"
14915 1. ooanbˇ
14916 2. zooanbˇ
14917 3. ooanbˇz
14918 4. zooanbˇz
14919 5. ooanˇ
14920 6. oanbˇ
14921
14922 ooanbˇ
14923 "};
14924 let completion_marked_buffer = indoc! {"
14925 1. ooanb
14926 2. zooanb
14927 3. ooanbz
14928 4. zooanbz
14929 5. ooan
14930 6. oanb
14931
14932 <ooanb|>
14933 "};
14934 let expected = indoc! {"
14935 1. foo_and_barˇ
14936 2. zfoo_and_barˇ
14937 3. foo_and_barˇz
14938 4. zfoo_and_barˇz
14939 5. ooanfoo_and_barˇ
14940 6. oanbfoo_and_barˇ
14941
14942 foo_and_barˇ
14943 "};
14944 cx.set_state(initial_state);
14945 cx.update_editor(|editor, window, cx| {
14946 editor.show_completions(&ShowCompletions, window, cx);
14947 });
14948 handle_completion_request_with_insert_and_replace(
14949 &mut cx,
14950 completion_marked_buffer,
14951 vec![(completion_text, completion_text)],
14952 Arc::new(AtomicUsize::new(0)),
14953 )
14954 .await;
14955 cx.condition(|editor, _| editor.context_menu_visible())
14956 .await;
14957 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14958 editor
14959 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14960 .unwrap()
14961 });
14962 cx.assert_editor_state(expected);
14963 handle_resolve_completion_request(&mut cx, None).await;
14964 apply_additional_edits.await.unwrap();
14965
14966 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14967 // (expects the same as if it was inserted at the end)
14968 let completion_text = "foo_and_bar";
14969 let initial_state = indoc! {"
14970 1. ooˇanb
14971 2. zooˇanb
14972 3. ooˇanbz
14973 4. zooˇanbz
14974
14975 ooˇanb
14976 "};
14977 let completion_marked_buffer = indoc! {"
14978 1. ooanb
14979 2. zooanb
14980 3. ooanbz
14981 4. zooanbz
14982
14983 <oo|anb>
14984 "};
14985 let expected = indoc! {"
14986 1. foo_and_barˇ
14987 2. zfoo_and_barˇ
14988 3. foo_and_barˇz
14989 4. zfoo_and_barˇz
14990
14991 foo_and_barˇ
14992 "};
14993 cx.set_state(initial_state);
14994 cx.update_editor(|editor, window, cx| {
14995 editor.show_completions(&ShowCompletions, window, cx);
14996 });
14997 handle_completion_request_with_insert_and_replace(
14998 &mut cx,
14999 completion_marked_buffer,
15000 vec![(completion_text, completion_text)],
15001 Arc::new(AtomicUsize::new(0)),
15002 )
15003 .await;
15004 cx.condition(|editor, _| editor.context_menu_visible())
15005 .await;
15006 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15007 editor
15008 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
15009 .unwrap()
15010 });
15011 cx.assert_editor_state(expected);
15012 handle_resolve_completion_request(&mut cx, None).await;
15013 apply_additional_edits.await.unwrap();
15014}
15015
15016// This used to crash
15017#[gpui::test]
15018async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
15019 init_test(cx, |_| {});
15020
15021 let buffer_text = indoc! {"
15022 fn main() {
15023 10.satu;
15024
15025 //
15026 // separate cursors so they open in different excerpts (manually reproducible)
15027 //
15028
15029 10.satu20;
15030 }
15031 "};
15032 let multibuffer_text_with_selections = indoc! {"
15033 fn main() {
15034 10.satuˇ;
15035
15036 //
15037
15038 //
15039
15040 10.satuˇ20;
15041 }
15042 "};
15043 let expected_multibuffer = indoc! {"
15044 fn main() {
15045 10.saturating_sub()ˇ;
15046
15047 //
15048
15049 //
15050
15051 10.saturating_sub()ˇ;
15052 }
15053 "};
15054
15055 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
15056 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
15057
15058 let fs = FakeFs::new(cx.executor());
15059 fs.insert_tree(
15060 path!("/a"),
15061 json!({
15062 "main.rs": buffer_text,
15063 }),
15064 )
15065 .await;
15066
15067 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15068 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15069 language_registry.add(rust_lang());
15070 let mut fake_servers = language_registry.register_fake_lsp(
15071 "Rust",
15072 FakeLspAdapter {
15073 capabilities: lsp::ServerCapabilities {
15074 completion_provider: Some(lsp::CompletionOptions {
15075 resolve_provider: None,
15076 ..lsp::CompletionOptions::default()
15077 }),
15078 ..lsp::ServerCapabilities::default()
15079 },
15080 ..FakeLspAdapter::default()
15081 },
15082 );
15083 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15084 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15085 let buffer = project
15086 .update(cx, |project, cx| {
15087 project.open_local_buffer(path!("/a/main.rs"), cx)
15088 })
15089 .await
15090 .unwrap();
15091
15092 let multi_buffer = cx.new(|cx| {
15093 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
15094 multi_buffer.push_excerpts(
15095 buffer.clone(),
15096 [ExcerptRange::new(0..first_excerpt_end)],
15097 cx,
15098 );
15099 multi_buffer.push_excerpts(
15100 buffer.clone(),
15101 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
15102 cx,
15103 );
15104 multi_buffer
15105 });
15106
15107 let editor = workspace
15108 .update(cx, |_, window, cx| {
15109 cx.new(|cx| {
15110 Editor::new(
15111 EditorMode::Full {
15112 scale_ui_elements_with_buffer_font_size: false,
15113 show_active_line_background: false,
15114 sizing_behavior: SizingBehavior::Default,
15115 },
15116 multi_buffer.clone(),
15117 Some(project.clone()),
15118 window,
15119 cx,
15120 )
15121 })
15122 })
15123 .unwrap();
15124
15125 let pane = workspace
15126 .update(cx, |workspace, _, _| workspace.active_pane().clone())
15127 .unwrap();
15128 pane.update_in(cx, |pane, window, cx| {
15129 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
15130 });
15131
15132 let fake_server = fake_servers.next().await.unwrap();
15133 cx.run_until_parked();
15134
15135 editor.update_in(cx, |editor, window, cx| {
15136 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15137 s.select_ranges([
15138 Point::new(1, 11)..Point::new(1, 11),
15139 Point::new(7, 11)..Point::new(7, 11),
15140 ])
15141 });
15142
15143 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
15144 });
15145
15146 editor.update_in(cx, |editor, window, cx| {
15147 editor.show_completions(&ShowCompletions, window, cx);
15148 });
15149
15150 fake_server
15151 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15152 let completion_item = lsp::CompletionItem {
15153 label: "saturating_sub()".into(),
15154 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
15155 lsp::InsertReplaceEdit {
15156 new_text: "saturating_sub()".to_owned(),
15157 insert: lsp::Range::new(
15158 lsp::Position::new(7, 7),
15159 lsp::Position::new(7, 11),
15160 ),
15161 replace: lsp::Range::new(
15162 lsp::Position::new(7, 7),
15163 lsp::Position::new(7, 13),
15164 ),
15165 },
15166 )),
15167 ..lsp::CompletionItem::default()
15168 };
15169
15170 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
15171 })
15172 .next()
15173 .await
15174 .unwrap();
15175
15176 cx.condition(&editor, |editor, _| editor.context_menu_visible())
15177 .await;
15178
15179 editor
15180 .update_in(cx, |editor, window, cx| {
15181 editor
15182 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
15183 .unwrap()
15184 })
15185 .await
15186 .unwrap();
15187
15188 editor.update(cx, |editor, cx| {
15189 assert_text_with_selections(editor, expected_multibuffer, cx);
15190 })
15191}
15192
15193#[gpui::test]
15194async fn test_completion(cx: &mut TestAppContext) {
15195 init_test(cx, |_| {});
15196
15197 let mut cx = EditorLspTestContext::new_rust(
15198 lsp::ServerCapabilities {
15199 completion_provider: Some(lsp::CompletionOptions {
15200 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15201 resolve_provider: Some(true),
15202 ..Default::default()
15203 }),
15204 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15205 ..Default::default()
15206 },
15207 cx,
15208 )
15209 .await;
15210 let counter = Arc::new(AtomicUsize::new(0));
15211
15212 cx.set_state(indoc! {"
15213 oneˇ
15214 two
15215 three
15216 "});
15217 cx.simulate_keystroke(".");
15218 handle_completion_request(
15219 indoc! {"
15220 one.|<>
15221 two
15222 three
15223 "},
15224 vec!["first_completion", "second_completion"],
15225 true,
15226 counter.clone(),
15227 &mut cx,
15228 )
15229 .await;
15230 cx.condition(|editor, _| editor.context_menu_visible())
15231 .await;
15232 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15233
15234 let _handler = handle_signature_help_request(
15235 &mut cx,
15236 lsp::SignatureHelp {
15237 signatures: vec![lsp::SignatureInformation {
15238 label: "test signature".to_string(),
15239 documentation: None,
15240 parameters: Some(vec![lsp::ParameterInformation {
15241 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
15242 documentation: None,
15243 }]),
15244 active_parameter: None,
15245 }],
15246 active_signature: None,
15247 active_parameter: None,
15248 },
15249 );
15250 cx.update_editor(|editor, window, cx| {
15251 assert!(
15252 !editor.signature_help_state.is_shown(),
15253 "No signature help was called for"
15254 );
15255 editor.show_signature_help(&ShowSignatureHelp, window, cx);
15256 });
15257 cx.run_until_parked();
15258 cx.update_editor(|editor, _, _| {
15259 assert!(
15260 !editor.signature_help_state.is_shown(),
15261 "No signature help should be shown when completions menu is open"
15262 );
15263 });
15264
15265 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15266 editor.context_menu_next(&Default::default(), window, cx);
15267 editor
15268 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15269 .unwrap()
15270 });
15271 cx.assert_editor_state(indoc! {"
15272 one.second_completionˇ
15273 two
15274 three
15275 "});
15276
15277 handle_resolve_completion_request(
15278 &mut cx,
15279 Some(vec![
15280 (
15281 //This overlaps with the primary completion edit which is
15282 //misbehavior from the LSP spec, test that we filter it out
15283 indoc! {"
15284 one.second_ˇcompletion
15285 two
15286 threeˇ
15287 "},
15288 "overlapping additional edit",
15289 ),
15290 (
15291 indoc! {"
15292 one.second_completion
15293 two
15294 threeˇ
15295 "},
15296 "\nadditional edit",
15297 ),
15298 ]),
15299 )
15300 .await;
15301 apply_additional_edits.await.unwrap();
15302 cx.assert_editor_state(indoc! {"
15303 one.second_completionˇ
15304 two
15305 three
15306 additional edit
15307 "});
15308
15309 cx.set_state(indoc! {"
15310 one.second_completion
15311 twoˇ
15312 threeˇ
15313 additional edit
15314 "});
15315 cx.simulate_keystroke(" ");
15316 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15317 cx.simulate_keystroke("s");
15318 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15319
15320 cx.assert_editor_state(indoc! {"
15321 one.second_completion
15322 two sˇ
15323 three sˇ
15324 additional edit
15325 "});
15326 handle_completion_request(
15327 indoc! {"
15328 one.second_completion
15329 two s
15330 three <s|>
15331 additional edit
15332 "},
15333 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
15334 true,
15335 counter.clone(),
15336 &mut cx,
15337 )
15338 .await;
15339 cx.condition(|editor, _| editor.context_menu_visible())
15340 .await;
15341 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
15342
15343 cx.simulate_keystroke("i");
15344
15345 handle_completion_request(
15346 indoc! {"
15347 one.second_completion
15348 two si
15349 three <si|>
15350 additional edit
15351 "},
15352 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
15353 true,
15354 counter.clone(),
15355 &mut cx,
15356 )
15357 .await;
15358 cx.condition(|editor, _| editor.context_menu_visible())
15359 .await;
15360 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15361
15362 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15363 editor
15364 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15365 .unwrap()
15366 });
15367 cx.assert_editor_state(indoc! {"
15368 one.second_completion
15369 two sixth_completionˇ
15370 three sixth_completionˇ
15371 additional edit
15372 "});
15373
15374 apply_additional_edits.await.unwrap();
15375
15376 update_test_language_settings(&mut cx, |settings| {
15377 settings.defaults.show_completions_on_input = Some(false);
15378 });
15379 cx.set_state("editorˇ");
15380 cx.simulate_keystroke(".");
15381 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15382 cx.simulate_keystrokes("c l o");
15383 cx.assert_editor_state("editor.cloˇ");
15384 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
15385 cx.update_editor(|editor, window, cx| {
15386 editor.show_completions(&ShowCompletions, window, cx);
15387 });
15388 handle_completion_request(
15389 "editor.<clo|>",
15390 vec!["close", "clobber"],
15391 true,
15392 counter.clone(),
15393 &mut cx,
15394 )
15395 .await;
15396 cx.condition(|editor, _| editor.context_menu_visible())
15397 .await;
15398 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
15399
15400 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
15401 editor
15402 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15403 .unwrap()
15404 });
15405 cx.assert_editor_state("editor.clobberˇ");
15406 handle_resolve_completion_request(&mut cx, None).await;
15407 apply_additional_edits.await.unwrap();
15408}
15409
15410#[gpui::test]
15411async fn test_completion_can_run_commands(cx: &mut TestAppContext) {
15412 init_test(cx, |_| {});
15413
15414 let fs = FakeFs::new(cx.executor());
15415 fs.insert_tree(
15416 path!("/a"),
15417 json!({
15418 "main.rs": "",
15419 }),
15420 )
15421 .await;
15422
15423 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15424 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15425 language_registry.add(rust_lang());
15426 let command_calls = Arc::new(AtomicUsize::new(0));
15427 let registered_command = "_the/command";
15428
15429 let closure_command_calls = command_calls.clone();
15430 let mut fake_servers = language_registry.register_fake_lsp(
15431 "Rust",
15432 FakeLspAdapter {
15433 capabilities: lsp::ServerCapabilities {
15434 completion_provider: Some(lsp::CompletionOptions {
15435 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15436 ..lsp::CompletionOptions::default()
15437 }),
15438 execute_command_provider: Some(lsp::ExecuteCommandOptions {
15439 commands: vec![registered_command.to_owned()],
15440 ..lsp::ExecuteCommandOptions::default()
15441 }),
15442 ..lsp::ServerCapabilities::default()
15443 },
15444 initializer: Some(Box::new(move |fake_server| {
15445 fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15446 move |params, _| async move {
15447 Ok(Some(lsp::CompletionResponse::Array(vec![
15448 lsp::CompletionItem {
15449 label: "registered_command".to_owned(),
15450 text_edit: gen_text_edit(¶ms, ""),
15451 command: Some(lsp::Command {
15452 title: registered_command.to_owned(),
15453 command: "_the/command".to_owned(),
15454 arguments: Some(vec![serde_json::Value::Bool(true)]),
15455 }),
15456 ..lsp::CompletionItem::default()
15457 },
15458 lsp::CompletionItem {
15459 label: "unregistered_command".to_owned(),
15460 text_edit: gen_text_edit(¶ms, ""),
15461 command: Some(lsp::Command {
15462 title: "????????????".to_owned(),
15463 command: "????????????".to_owned(),
15464 arguments: Some(vec![serde_json::Value::Null]),
15465 }),
15466 ..lsp::CompletionItem::default()
15467 },
15468 ])))
15469 },
15470 );
15471 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
15472 let command_calls = closure_command_calls.clone();
15473 move |params, _| {
15474 assert_eq!(params.command, registered_command);
15475 let command_calls = command_calls.clone();
15476 async move {
15477 command_calls.fetch_add(1, atomic::Ordering::Release);
15478 Ok(Some(json!(null)))
15479 }
15480 }
15481 });
15482 })),
15483 ..FakeLspAdapter::default()
15484 },
15485 );
15486 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15487 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15488 let editor = workspace
15489 .update(cx, |workspace, window, cx| {
15490 workspace.open_abs_path(
15491 PathBuf::from(path!("/a/main.rs")),
15492 OpenOptions::default(),
15493 window,
15494 cx,
15495 )
15496 })
15497 .unwrap()
15498 .await
15499 .unwrap()
15500 .downcast::<Editor>()
15501 .unwrap();
15502 let _fake_server = fake_servers.next().await.unwrap();
15503 cx.run_until_parked();
15504
15505 editor.update_in(cx, |editor, window, cx| {
15506 cx.focus_self(window);
15507 editor.move_to_end(&MoveToEnd, window, cx);
15508 editor.handle_input(".", window, cx);
15509 });
15510 cx.run_until_parked();
15511 editor.update(cx, |editor, _| {
15512 assert!(editor.context_menu_visible());
15513 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15514 {
15515 let completion_labels = menu
15516 .completions
15517 .borrow()
15518 .iter()
15519 .map(|c| c.label.text.clone())
15520 .collect::<Vec<_>>();
15521 assert_eq!(
15522 completion_labels,
15523 &["registered_command", "unregistered_command",],
15524 );
15525 } else {
15526 panic!("expected completion menu to be open");
15527 }
15528 });
15529
15530 editor
15531 .update_in(cx, |editor, window, cx| {
15532 editor
15533 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15534 .unwrap()
15535 })
15536 .await
15537 .unwrap();
15538 cx.run_until_parked();
15539 assert_eq!(
15540 command_calls.load(atomic::Ordering::Acquire),
15541 1,
15542 "For completion with a registered command, Zed should send a command execution request",
15543 );
15544
15545 editor.update_in(cx, |editor, window, cx| {
15546 cx.focus_self(window);
15547 editor.handle_input(".", window, cx);
15548 });
15549 cx.run_until_parked();
15550 editor.update(cx, |editor, _| {
15551 assert!(editor.context_menu_visible());
15552 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15553 {
15554 let completion_labels = menu
15555 .completions
15556 .borrow()
15557 .iter()
15558 .map(|c| c.label.text.clone())
15559 .collect::<Vec<_>>();
15560 assert_eq!(
15561 completion_labels,
15562 &["registered_command", "unregistered_command",],
15563 );
15564 } else {
15565 panic!("expected completion menu to be open");
15566 }
15567 });
15568 editor
15569 .update_in(cx, |editor, window, cx| {
15570 editor.context_menu_next(&Default::default(), window, cx);
15571 editor
15572 .confirm_completion(&ConfirmCompletion::default(), window, cx)
15573 .unwrap()
15574 })
15575 .await
15576 .unwrap();
15577 cx.run_until_parked();
15578 assert_eq!(
15579 command_calls.load(atomic::Ordering::Acquire),
15580 1,
15581 "For completion with an unregistered command, Zed should not send a command execution request",
15582 );
15583}
15584
15585#[gpui::test]
15586async fn test_completion_reuse(cx: &mut TestAppContext) {
15587 init_test(cx, |_| {});
15588
15589 let mut cx = EditorLspTestContext::new_rust(
15590 lsp::ServerCapabilities {
15591 completion_provider: Some(lsp::CompletionOptions {
15592 trigger_characters: Some(vec![".".to_string()]),
15593 ..Default::default()
15594 }),
15595 ..Default::default()
15596 },
15597 cx,
15598 )
15599 .await;
15600
15601 let counter = Arc::new(AtomicUsize::new(0));
15602 cx.set_state("objˇ");
15603 cx.simulate_keystroke(".");
15604
15605 // Initial completion request returns complete results
15606 let is_incomplete = false;
15607 handle_completion_request(
15608 "obj.|<>",
15609 vec!["a", "ab", "abc"],
15610 is_incomplete,
15611 counter.clone(),
15612 &mut cx,
15613 )
15614 .await;
15615 cx.run_until_parked();
15616 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15617 cx.assert_editor_state("obj.ˇ");
15618 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15619
15620 // Type "a" - filters existing completions
15621 cx.simulate_keystroke("a");
15622 cx.run_until_parked();
15623 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15624 cx.assert_editor_state("obj.aˇ");
15625 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15626
15627 // Type "b" - filters existing completions
15628 cx.simulate_keystroke("b");
15629 cx.run_until_parked();
15630 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15631 cx.assert_editor_state("obj.abˇ");
15632 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15633
15634 // Type "c" - filters existing completions
15635 cx.simulate_keystroke("c");
15636 cx.run_until_parked();
15637 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15638 cx.assert_editor_state("obj.abcˇ");
15639 check_displayed_completions(vec!["abc"], &mut cx);
15640
15641 // Backspace to delete "c" - filters existing completions
15642 cx.update_editor(|editor, window, cx| {
15643 editor.backspace(&Backspace, window, cx);
15644 });
15645 cx.run_until_parked();
15646 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15647 cx.assert_editor_state("obj.abˇ");
15648 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15649
15650 // Moving cursor to the left dismisses menu.
15651 cx.update_editor(|editor, window, cx| {
15652 editor.move_left(&MoveLeft, window, cx);
15653 });
15654 cx.run_until_parked();
15655 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15656 cx.assert_editor_state("obj.aˇb");
15657 cx.update_editor(|editor, _, _| {
15658 assert_eq!(editor.context_menu_visible(), false);
15659 });
15660
15661 // Type "b" - new request
15662 cx.simulate_keystroke("b");
15663 let is_incomplete = false;
15664 handle_completion_request(
15665 "obj.<ab|>a",
15666 vec!["ab", "abc"],
15667 is_incomplete,
15668 counter.clone(),
15669 &mut cx,
15670 )
15671 .await;
15672 cx.run_until_parked();
15673 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
15674 cx.assert_editor_state("obj.abˇb");
15675 check_displayed_completions(vec!["ab", "abc"], &mut cx);
15676
15677 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
15678 cx.update_editor(|editor, window, cx| {
15679 editor.backspace(&Backspace, window, cx);
15680 });
15681 let is_incomplete = false;
15682 handle_completion_request(
15683 "obj.<a|>b",
15684 vec!["a", "ab", "abc"],
15685 is_incomplete,
15686 counter.clone(),
15687 &mut cx,
15688 )
15689 .await;
15690 cx.run_until_parked();
15691 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15692 cx.assert_editor_state("obj.aˇb");
15693 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
15694
15695 // Backspace to delete "a" - dismisses menu.
15696 cx.update_editor(|editor, window, cx| {
15697 editor.backspace(&Backspace, window, cx);
15698 });
15699 cx.run_until_parked();
15700 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
15701 cx.assert_editor_state("obj.ˇb");
15702 cx.update_editor(|editor, _, _| {
15703 assert_eq!(editor.context_menu_visible(), false);
15704 });
15705}
15706
15707#[gpui::test]
15708async fn test_word_completion(cx: &mut TestAppContext) {
15709 let lsp_fetch_timeout_ms = 10;
15710 init_test(cx, |language_settings| {
15711 language_settings.defaults.completions = Some(CompletionSettingsContent {
15712 words_min_length: Some(0),
15713 lsp_fetch_timeout_ms: Some(10),
15714 lsp_insert_mode: Some(LspInsertMode::Insert),
15715 ..Default::default()
15716 });
15717 });
15718
15719 let mut cx = EditorLspTestContext::new_rust(
15720 lsp::ServerCapabilities {
15721 completion_provider: Some(lsp::CompletionOptions {
15722 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15723 ..lsp::CompletionOptions::default()
15724 }),
15725 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15726 ..lsp::ServerCapabilities::default()
15727 },
15728 cx,
15729 )
15730 .await;
15731
15732 let throttle_completions = Arc::new(AtomicBool::new(false));
15733
15734 let lsp_throttle_completions = throttle_completions.clone();
15735 let _completion_requests_handler =
15736 cx.lsp
15737 .server
15738 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
15739 let lsp_throttle_completions = lsp_throttle_completions.clone();
15740 let cx = cx.clone();
15741 async move {
15742 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
15743 cx.background_executor()
15744 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
15745 .await;
15746 }
15747 Ok(Some(lsp::CompletionResponse::Array(vec![
15748 lsp::CompletionItem {
15749 label: "first".into(),
15750 ..lsp::CompletionItem::default()
15751 },
15752 lsp::CompletionItem {
15753 label: "last".into(),
15754 ..lsp::CompletionItem::default()
15755 },
15756 ])))
15757 }
15758 });
15759
15760 cx.set_state(indoc! {"
15761 oneˇ
15762 two
15763 three
15764 "});
15765 cx.simulate_keystroke(".");
15766 cx.executor().run_until_parked();
15767 cx.condition(|editor, _| editor.context_menu_visible())
15768 .await;
15769 cx.update_editor(|editor, window, cx| {
15770 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15771 {
15772 assert_eq!(
15773 completion_menu_entries(menu),
15774 &["first", "last"],
15775 "When LSP server is fast to reply, no fallback word completions are used"
15776 );
15777 } else {
15778 panic!("expected completion menu to be open");
15779 }
15780 editor.cancel(&Cancel, window, cx);
15781 });
15782 cx.executor().run_until_parked();
15783 cx.condition(|editor, _| !editor.context_menu_visible())
15784 .await;
15785
15786 throttle_completions.store(true, atomic::Ordering::Release);
15787 cx.simulate_keystroke(".");
15788 cx.executor()
15789 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
15790 cx.executor().run_until_parked();
15791 cx.condition(|editor, _| editor.context_menu_visible())
15792 .await;
15793 cx.update_editor(|editor, _, _| {
15794 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15795 {
15796 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
15797 "When LSP server is slow, document words can be shown instead, if configured accordingly");
15798 } else {
15799 panic!("expected completion menu to be open");
15800 }
15801 });
15802}
15803
15804#[gpui::test]
15805async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
15806 init_test(cx, |language_settings| {
15807 language_settings.defaults.completions = Some(CompletionSettingsContent {
15808 words: Some(WordsCompletionMode::Enabled),
15809 words_min_length: Some(0),
15810 lsp_insert_mode: Some(LspInsertMode::Insert),
15811 ..Default::default()
15812 });
15813 });
15814
15815 let mut cx = EditorLspTestContext::new_rust(
15816 lsp::ServerCapabilities {
15817 completion_provider: Some(lsp::CompletionOptions {
15818 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15819 ..lsp::CompletionOptions::default()
15820 }),
15821 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15822 ..lsp::ServerCapabilities::default()
15823 },
15824 cx,
15825 )
15826 .await;
15827
15828 let _completion_requests_handler =
15829 cx.lsp
15830 .server
15831 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15832 Ok(Some(lsp::CompletionResponse::Array(vec![
15833 lsp::CompletionItem {
15834 label: "first".into(),
15835 ..lsp::CompletionItem::default()
15836 },
15837 lsp::CompletionItem {
15838 label: "last".into(),
15839 ..lsp::CompletionItem::default()
15840 },
15841 ])))
15842 });
15843
15844 cx.set_state(indoc! {"ˇ
15845 first
15846 last
15847 second
15848 "});
15849 cx.simulate_keystroke(".");
15850 cx.executor().run_until_parked();
15851 cx.condition(|editor, _| editor.context_menu_visible())
15852 .await;
15853 cx.update_editor(|editor, _, _| {
15854 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15855 {
15856 assert_eq!(
15857 completion_menu_entries(menu),
15858 &["first", "last", "second"],
15859 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
15860 );
15861 } else {
15862 panic!("expected completion menu to be open");
15863 }
15864 });
15865}
15866
15867#[gpui::test]
15868async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
15869 init_test(cx, |language_settings| {
15870 language_settings.defaults.completions = Some(CompletionSettingsContent {
15871 words: Some(WordsCompletionMode::Disabled),
15872 words_min_length: Some(0),
15873 lsp_insert_mode: Some(LspInsertMode::Insert),
15874 ..Default::default()
15875 });
15876 });
15877
15878 let mut cx = EditorLspTestContext::new_rust(
15879 lsp::ServerCapabilities {
15880 completion_provider: Some(lsp::CompletionOptions {
15881 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15882 ..lsp::CompletionOptions::default()
15883 }),
15884 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15885 ..lsp::ServerCapabilities::default()
15886 },
15887 cx,
15888 )
15889 .await;
15890
15891 let _completion_requests_handler =
15892 cx.lsp
15893 .server
15894 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
15895 panic!("LSP completions should not be queried when dealing with word completions")
15896 });
15897
15898 cx.set_state(indoc! {"ˇ
15899 first
15900 last
15901 second
15902 "});
15903 cx.update_editor(|editor, window, cx| {
15904 editor.show_word_completions(&ShowWordCompletions, window, cx);
15905 });
15906 cx.executor().run_until_parked();
15907 cx.condition(|editor, _| editor.context_menu_visible())
15908 .await;
15909 cx.update_editor(|editor, _, _| {
15910 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15911 {
15912 assert_eq!(
15913 completion_menu_entries(menu),
15914 &["first", "last", "second"],
15915 "`ShowWordCompletions` action should show word completions"
15916 );
15917 } else {
15918 panic!("expected completion menu to be open");
15919 }
15920 });
15921
15922 cx.simulate_keystroke("l");
15923 cx.executor().run_until_parked();
15924 cx.condition(|editor, _| editor.context_menu_visible())
15925 .await;
15926 cx.update_editor(|editor, _, _| {
15927 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15928 {
15929 assert_eq!(
15930 completion_menu_entries(menu),
15931 &["last"],
15932 "After showing word completions, further editing should filter them and not query the LSP"
15933 );
15934 } else {
15935 panic!("expected completion menu to be open");
15936 }
15937 });
15938}
15939
15940#[gpui::test]
15941async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
15942 init_test(cx, |language_settings| {
15943 language_settings.defaults.completions = Some(CompletionSettingsContent {
15944 words_min_length: Some(0),
15945 lsp: Some(false),
15946 lsp_insert_mode: Some(LspInsertMode::Insert),
15947 ..Default::default()
15948 });
15949 });
15950
15951 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15952
15953 cx.set_state(indoc! {"ˇ
15954 0_usize
15955 let
15956 33
15957 4.5f32
15958 "});
15959 cx.update_editor(|editor, window, cx| {
15960 editor.show_completions(&ShowCompletions, window, cx);
15961 });
15962 cx.executor().run_until_parked();
15963 cx.condition(|editor, _| editor.context_menu_visible())
15964 .await;
15965 cx.update_editor(|editor, window, cx| {
15966 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15967 {
15968 assert_eq!(
15969 completion_menu_entries(menu),
15970 &["let"],
15971 "With no digits in the completion query, no digits should be in the word completions"
15972 );
15973 } else {
15974 panic!("expected completion menu to be open");
15975 }
15976 editor.cancel(&Cancel, window, cx);
15977 });
15978
15979 cx.set_state(indoc! {"3ˇ
15980 0_usize
15981 let
15982 3
15983 33.35f32
15984 "});
15985 cx.update_editor(|editor, window, cx| {
15986 editor.show_completions(&ShowCompletions, window, cx);
15987 });
15988 cx.executor().run_until_parked();
15989 cx.condition(|editor, _| editor.context_menu_visible())
15990 .await;
15991 cx.update_editor(|editor, _, _| {
15992 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15993 {
15994 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
15995 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
15996 } else {
15997 panic!("expected completion menu to be open");
15998 }
15999 });
16000}
16001
16002#[gpui::test]
16003async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
16004 init_test(cx, |language_settings| {
16005 language_settings.defaults.completions = Some(CompletionSettingsContent {
16006 words: Some(WordsCompletionMode::Enabled),
16007 words_min_length: Some(3),
16008 lsp_insert_mode: Some(LspInsertMode::Insert),
16009 ..Default::default()
16010 });
16011 });
16012
16013 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16014 cx.set_state(indoc! {"ˇ
16015 wow
16016 wowen
16017 wowser
16018 "});
16019 cx.simulate_keystroke("w");
16020 cx.executor().run_until_parked();
16021 cx.update_editor(|editor, _, _| {
16022 if editor.context_menu.borrow_mut().is_some() {
16023 panic!(
16024 "expected completion menu to be hidden, as words completion threshold is not met"
16025 );
16026 }
16027 });
16028
16029 cx.update_editor(|editor, window, cx| {
16030 editor.show_word_completions(&ShowWordCompletions, window, cx);
16031 });
16032 cx.executor().run_until_parked();
16033 cx.update_editor(|editor, window, cx| {
16034 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16035 {
16036 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");
16037 } else {
16038 panic!("expected completion menu to be open after the word completions are called with an action");
16039 }
16040
16041 editor.cancel(&Cancel, window, cx);
16042 });
16043 cx.update_editor(|editor, _, _| {
16044 if editor.context_menu.borrow_mut().is_some() {
16045 panic!("expected completion menu to be hidden after canceling");
16046 }
16047 });
16048
16049 cx.simulate_keystroke("o");
16050 cx.executor().run_until_parked();
16051 cx.update_editor(|editor, _, _| {
16052 if editor.context_menu.borrow_mut().is_some() {
16053 panic!(
16054 "expected completion menu to be hidden, as words completion threshold is not met still"
16055 );
16056 }
16057 });
16058
16059 cx.simulate_keystroke("w");
16060 cx.executor().run_until_parked();
16061 cx.update_editor(|editor, _, _| {
16062 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16063 {
16064 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
16065 } else {
16066 panic!("expected completion menu to be open after the word completions threshold is met");
16067 }
16068 });
16069}
16070
16071#[gpui::test]
16072async fn test_word_completions_disabled(cx: &mut TestAppContext) {
16073 init_test(cx, |language_settings| {
16074 language_settings.defaults.completions = Some(CompletionSettingsContent {
16075 words: Some(WordsCompletionMode::Enabled),
16076 words_min_length: Some(0),
16077 lsp_insert_mode: Some(LspInsertMode::Insert),
16078 ..Default::default()
16079 });
16080 });
16081
16082 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16083 cx.update_editor(|editor, _, _| {
16084 editor.disable_word_completions();
16085 });
16086 cx.set_state(indoc! {"ˇ
16087 wow
16088 wowen
16089 wowser
16090 "});
16091 cx.simulate_keystroke("w");
16092 cx.executor().run_until_parked();
16093 cx.update_editor(|editor, _, _| {
16094 if editor.context_menu.borrow_mut().is_some() {
16095 panic!(
16096 "expected completion menu to be hidden, as words completion are disabled for this editor"
16097 );
16098 }
16099 });
16100
16101 cx.update_editor(|editor, window, cx| {
16102 editor.show_word_completions(&ShowWordCompletions, window, cx);
16103 });
16104 cx.executor().run_until_parked();
16105 cx.update_editor(|editor, _, _| {
16106 if editor.context_menu.borrow_mut().is_some() {
16107 panic!(
16108 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
16109 );
16110 }
16111 });
16112}
16113
16114#[gpui::test]
16115async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
16116 init_test(cx, |language_settings| {
16117 language_settings.defaults.completions = Some(CompletionSettingsContent {
16118 words: Some(WordsCompletionMode::Disabled),
16119 words_min_length: Some(0),
16120 lsp_insert_mode: Some(LspInsertMode::Insert),
16121 ..Default::default()
16122 });
16123 });
16124
16125 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
16126 cx.update_editor(|editor, _, _| {
16127 editor.set_completion_provider(None);
16128 });
16129 cx.set_state(indoc! {"ˇ
16130 wow
16131 wowen
16132 wowser
16133 "});
16134 cx.simulate_keystroke("w");
16135 cx.executor().run_until_parked();
16136 cx.update_editor(|editor, _, _| {
16137 if editor.context_menu.borrow_mut().is_some() {
16138 panic!("expected completion menu to be hidden, as disabled in settings");
16139 }
16140 });
16141}
16142
16143fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
16144 let position = || lsp::Position {
16145 line: params.text_document_position.position.line,
16146 character: params.text_document_position.position.character,
16147 };
16148 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16149 range: lsp::Range {
16150 start: position(),
16151 end: position(),
16152 },
16153 new_text: text.to_string(),
16154 }))
16155}
16156
16157#[gpui::test]
16158async fn test_multiline_completion(cx: &mut TestAppContext) {
16159 init_test(cx, |_| {});
16160
16161 let fs = FakeFs::new(cx.executor());
16162 fs.insert_tree(
16163 path!("/a"),
16164 json!({
16165 "main.ts": "a",
16166 }),
16167 )
16168 .await;
16169
16170 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16171 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16172 let typescript_language = Arc::new(Language::new(
16173 LanguageConfig {
16174 name: "TypeScript".into(),
16175 matcher: LanguageMatcher {
16176 path_suffixes: vec!["ts".to_string()],
16177 ..LanguageMatcher::default()
16178 },
16179 line_comments: vec!["// ".into()],
16180 ..LanguageConfig::default()
16181 },
16182 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
16183 ));
16184 language_registry.add(typescript_language.clone());
16185 let mut fake_servers = language_registry.register_fake_lsp(
16186 "TypeScript",
16187 FakeLspAdapter {
16188 capabilities: lsp::ServerCapabilities {
16189 completion_provider: Some(lsp::CompletionOptions {
16190 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
16191 ..lsp::CompletionOptions::default()
16192 }),
16193 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
16194 ..lsp::ServerCapabilities::default()
16195 },
16196 // Emulate vtsls label generation
16197 label_for_completion: Some(Box::new(|item, _| {
16198 let text = if let Some(description) = item
16199 .label_details
16200 .as_ref()
16201 .and_then(|label_details| label_details.description.as_ref())
16202 {
16203 format!("{} {}", item.label, description)
16204 } else if let Some(detail) = &item.detail {
16205 format!("{} {}", item.label, detail)
16206 } else {
16207 item.label.clone()
16208 };
16209 Some(language::CodeLabel::plain(text, None))
16210 })),
16211 ..FakeLspAdapter::default()
16212 },
16213 );
16214 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16215 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16216 let worktree_id = workspace
16217 .update(cx, |workspace, _window, cx| {
16218 workspace.project().update(cx, |project, cx| {
16219 project.worktrees(cx).next().unwrap().read(cx).id()
16220 })
16221 })
16222 .unwrap();
16223 let _buffer = project
16224 .update(cx, |project, cx| {
16225 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
16226 })
16227 .await
16228 .unwrap();
16229 let editor = workspace
16230 .update(cx, |workspace, window, cx| {
16231 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
16232 })
16233 .unwrap()
16234 .await
16235 .unwrap()
16236 .downcast::<Editor>()
16237 .unwrap();
16238 let fake_server = fake_servers.next().await.unwrap();
16239 cx.run_until_parked();
16240
16241 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
16242 let multiline_label_2 = "a\nb\nc\n";
16243 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
16244 let multiline_description = "d\ne\nf\n";
16245 let multiline_detail_2 = "g\nh\ni\n";
16246
16247 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
16248 move |params, _| async move {
16249 Ok(Some(lsp::CompletionResponse::Array(vec![
16250 lsp::CompletionItem {
16251 label: multiline_label.to_string(),
16252 text_edit: gen_text_edit(¶ms, "new_text_1"),
16253 ..lsp::CompletionItem::default()
16254 },
16255 lsp::CompletionItem {
16256 label: "single line label 1".to_string(),
16257 detail: Some(multiline_detail.to_string()),
16258 text_edit: gen_text_edit(¶ms, "new_text_2"),
16259 ..lsp::CompletionItem::default()
16260 },
16261 lsp::CompletionItem {
16262 label: "single line label 2".to_string(),
16263 label_details: Some(lsp::CompletionItemLabelDetails {
16264 description: Some(multiline_description.to_string()),
16265 detail: None,
16266 }),
16267 text_edit: gen_text_edit(¶ms, "new_text_2"),
16268 ..lsp::CompletionItem::default()
16269 },
16270 lsp::CompletionItem {
16271 label: multiline_label_2.to_string(),
16272 detail: Some(multiline_detail_2.to_string()),
16273 text_edit: gen_text_edit(¶ms, "new_text_3"),
16274 ..lsp::CompletionItem::default()
16275 },
16276 lsp::CompletionItem {
16277 label: "Label with many spaces and \t but without newlines".to_string(),
16278 detail: Some(
16279 "Details with many spaces and \t but without newlines".to_string(),
16280 ),
16281 text_edit: gen_text_edit(¶ms, "new_text_4"),
16282 ..lsp::CompletionItem::default()
16283 },
16284 ])))
16285 },
16286 );
16287
16288 editor.update_in(cx, |editor, window, cx| {
16289 cx.focus_self(window);
16290 editor.move_to_end(&MoveToEnd, window, cx);
16291 editor.handle_input(".", window, cx);
16292 });
16293 cx.run_until_parked();
16294 completion_handle.next().await.unwrap();
16295
16296 editor.update(cx, |editor, _| {
16297 assert!(editor.context_menu_visible());
16298 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16299 {
16300 let completion_labels = menu
16301 .completions
16302 .borrow()
16303 .iter()
16304 .map(|c| c.label.text.clone())
16305 .collect::<Vec<_>>();
16306 assert_eq!(
16307 completion_labels,
16308 &[
16309 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
16310 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
16311 "single line label 2 d e f ",
16312 "a b c g h i ",
16313 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
16314 ],
16315 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
16316 );
16317
16318 for completion in menu
16319 .completions
16320 .borrow()
16321 .iter() {
16322 assert_eq!(
16323 completion.label.filter_range,
16324 0..completion.label.text.len(),
16325 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
16326 );
16327 }
16328 } else {
16329 panic!("expected completion menu to be open");
16330 }
16331 });
16332}
16333
16334#[gpui::test]
16335async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
16336 init_test(cx, |_| {});
16337 let mut cx = EditorLspTestContext::new_rust(
16338 lsp::ServerCapabilities {
16339 completion_provider: Some(lsp::CompletionOptions {
16340 trigger_characters: Some(vec![".".to_string()]),
16341 ..Default::default()
16342 }),
16343 ..Default::default()
16344 },
16345 cx,
16346 )
16347 .await;
16348 cx.lsp
16349 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16350 Ok(Some(lsp::CompletionResponse::Array(vec![
16351 lsp::CompletionItem {
16352 label: "first".into(),
16353 ..Default::default()
16354 },
16355 lsp::CompletionItem {
16356 label: "last".into(),
16357 ..Default::default()
16358 },
16359 ])))
16360 });
16361 cx.set_state("variableˇ");
16362 cx.simulate_keystroke(".");
16363 cx.executor().run_until_parked();
16364
16365 cx.update_editor(|editor, _, _| {
16366 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16367 {
16368 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
16369 } else {
16370 panic!("expected completion menu to be open");
16371 }
16372 });
16373
16374 cx.update_editor(|editor, window, cx| {
16375 editor.move_page_down(&MovePageDown::default(), window, cx);
16376 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16377 {
16378 assert!(
16379 menu.selected_item == 1,
16380 "expected PageDown to select the last item from the context menu"
16381 );
16382 } else {
16383 panic!("expected completion menu to stay open after PageDown");
16384 }
16385 });
16386
16387 cx.update_editor(|editor, window, cx| {
16388 editor.move_page_up(&MovePageUp::default(), window, cx);
16389 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
16390 {
16391 assert!(
16392 menu.selected_item == 0,
16393 "expected PageUp to select the first item from the context menu"
16394 );
16395 } else {
16396 panic!("expected completion menu to stay open after PageUp");
16397 }
16398 });
16399}
16400
16401#[gpui::test]
16402async fn test_as_is_completions(cx: &mut TestAppContext) {
16403 init_test(cx, |_| {});
16404 let mut cx = EditorLspTestContext::new_rust(
16405 lsp::ServerCapabilities {
16406 completion_provider: Some(lsp::CompletionOptions {
16407 ..Default::default()
16408 }),
16409 ..Default::default()
16410 },
16411 cx,
16412 )
16413 .await;
16414 cx.lsp
16415 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16416 Ok(Some(lsp::CompletionResponse::Array(vec![
16417 lsp::CompletionItem {
16418 label: "unsafe".into(),
16419 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16420 range: lsp::Range {
16421 start: lsp::Position {
16422 line: 1,
16423 character: 2,
16424 },
16425 end: lsp::Position {
16426 line: 1,
16427 character: 3,
16428 },
16429 },
16430 new_text: "unsafe".to_string(),
16431 })),
16432 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
16433 ..Default::default()
16434 },
16435 ])))
16436 });
16437 cx.set_state("fn a() {}\n nˇ");
16438 cx.executor().run_until_parked();
16439 cx.update_editor(|editor, window, cx| {
16440 editor.trigger_completion_on_input("n", true, window, cx)
16441 });
16442 cx.executor().run_until_parked();
16443
16444 cx.update_editor(|editor, window, cx| {
16445 editor.confirm_completion(&Default::default(), window, cx)
16446 });
16447 cx.executor().run_until_parked();
16448 cx.assert_editor_state("fn a() {}\n unsafeˇ");
16449}
16450
16451#[gpui::test]
16452async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
16453 init_test(cx, |_| {});
16454 let language =
16455 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
16456 let mut cx = EditorLspTestContext::new(
16457 language,
16458 lsp::ServerCapabilities {
16459 completion_provider: Some(lsp::CompletionOptions {
16460 ..lsp::CompletionOptions::default()
16461 }),
16462 ..lsp::ServerCapabilities::default()
16463 },
16464 cx,
16465 )
16466 .await;
16467
16468 cx.set_state(
16469 "#ifndef BAR_H
16470#define BAR_H
16471
16472#include <stdbool.h>
16473
16474int fn_branch(bool do_branch1, bool do_branch2);
16475
16476#endif // BAR_H
16477ˇ",
16478 );
16479 cx.executor().run_until_parked();
16480 cx.update_editor(|editor, window, cx| {
16481 editor.handle_input("#", window, cx);
16482 });
16483 cx.executor().run_until_parked();
16484 cx.update_editor(|editor, window, cx| {
16485 editor.handle_input("i", window, cx);
16486 });
16487 cx.executor().run_until_parked();
16488 cx.update_editor(|editor, window, cx| {
16489 editor.handle_input("n", window, cx);
16490 });
16491 cx.executor().run_until_parked();
16492 cx.assert_editor_state(
16493 "#ifndef BAR_H
16494#define BAR_H
16495
16496#include <stdbool.h>
16497
16498int fn_branch(bool do_branch1, bool do_branch2);
16499
16500#endif // BAR_H
16501#inˇ",
16502 );
16503
16504 cx.lsp
16505 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16506 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16507 is_incomplete: false,
16508 item_defaults: None,
16509 items: vec![lsp::CompletionItem {
16510 kind: Some(lsp::CompletionItemKind::SNIPPET),
16511 label_details: Some(lsp::CompletionItemLabelDetails {
16512 detail: Some("header".to_string()),
16513 description: None,
16514 }),
16515 label: " include".to_string(),
16516 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16517 range: lsp::Range {
16518 start: lsp::Position {
16519 line: 8,
16520 character: 1,
16521 },
16522 end: lsp::Position {
16523 line: 8,
16524 character: 1,
16525 },
16526 },
16527 new_text: "include \"$0\"".to_string(),
16528 })),
16529 sort_text: Some("40b67681include".to_string()),
16530 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16531 filter_text: Some("include".to_string()),
16532 insert_text: Some("include \"$0\"".to_string()),
16533 ..lsp::CompletionItem::default()
16534 }],
16535 })))
16536 });
16537 cx.update_editor(|editor, window, cx| {
16538 editor.show_completions(&ShowCompletions, window, cx);
16539 });
16540 cx.executor().run_until_parked();
16541 cx.update_editor(|editor, window, cx| {
16542 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16543 });
16544 cx.executor().run_until_parked();
16545 cx.assert_editor_state(
16546 "#ifndef BAR_H
16547#define BAR_H
16548
16549#include <stdbool.h>
16550
16551int fn_branch(bool do_branch1, bool do_branch2);
16552
16553#endif // BAR_H
16554#include \"ˇ\"",
16555 );
16556
16557 cx.lsp
16558 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
16559 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16560 is_incomplete: true,
16561 item_defaults: None,
16562 items: vec![lsp::CompletionItem {
16563 kind: Some(lsp::CompletionItemKind::FILE),
16564 label: "AGL/".to_string(),
16565 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16566 range: lsp::Range {
16567 start: lsp::Position {
16568 line: 8,
16569 character: 10,
16570 },
16571 end: lsp::Position {
16572 line: 8,
16573 character: 11,
16574 },
16575 },
16576 new_text: "AGL/".to_string(),
16577 })),
16578 sort_text: Some("40b67681AGL/".to_string()),
16579 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
16580 filter_text: Some("AGL/".to_string()),
16581 insert_text: Some("AGL/".to_string()),
16582 ..lsp::CompletionItem::default()
16583 }],
16584 })))
16585 });
16586 cx.update_editor(|editor, window, cx| {
16587 editor.show_completions(&ShowCompletions, window, cx);
16588 });
16589 cx.executor().run_until_parked();
16590 cx.update_editor(|editor, window, cx| {
16591 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
16592 });
16593 cx.executor().run_until_parked();
16594 cx.assert_editor_state(
16595 r##"#ifndef BAR_H
16596#define BAR_H
16597
16598#include <stdbool.h>
16599
16600int fn_branch(bool do_branch1, bool do_branch2);
16601
16602#endif // BAR_H
16603#include "AGL/ˇ"##,
16604 );
16605
16606 cx.update_editor(|editor, window, cx| {
16607 editor.handle_input("\"", window, cx);
16608 });
16609 cx.executor().run_until_parked();
16610 cx.assert_editor_state(
16611 r##"#ifndef BAR_H
16612#define BAR_H
16613
16614#include <stdbool.h>
16615
16616int fn_branch(bool do_branch1, bool do_branch2);
16617
16618#endif // BAR_H
16619#include "AGL/"ˇ"##,
16620 );
16621}
16622
16623#[gpui::test]
16624async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
16625 init_test(cx, |_| {});
16626
16627 let mut cx = EditorLspTestContext::new_rust(
16628 lsp::ServerCapabilities {
16629 completion_provider: Some(lsp::CompletionOptions {
16630 trigger_characters: Some(vec![".".to_string()]),
16631 resolve_provider: Some(false),
16632 ..lsp::CompletionOptions::default()
16633 }),
16634 ..lsp::ServerCapabilities::default()
16635 },
16636 cx,
16637 )
16638 .await;
16639
16640 cx.set_state("fn main() { let a = 2ˇ; }");
16641 cx.simulate_keystroke(".");
16642 let completion_item = lsp::CompletionItem {
16643 label: "Some".into(),
16644 kind: Some(lsp::CompletionItemKind::SNIPPET),
16645 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
16646 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
16647 kind: lsp::MarkupKind::Markdown,
16648 value: "```rust\nSome(2)\n```".to_string(),
16649 })),
16650 deprecated: Some(false),
16651 sort_text: Some("Some".to_string()),
16652 filter_text: Some("Some".to_string()),
16653 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
16654 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
16655 range: lsp::Range {
16656 start: lsp::Position {
16657 line: 0,
16658 character: 22,
16659 },
16660 end: lsp::Position {
16661 line: 0,
16662 character: 22,
16663 },
16664 },
16665 new_text: "Some(2)".to_string(),
16666 })),
16667 additional_text_edits: Some(vec![lsp::TextEdit {
16668 range: lsp::Range {
16669 start: lsp::Position {
16670 line: 0,
16671 character: 20,
16672 },
16673 end: lsp::Position {
16674 line: 0,
16675 character: 22,
16676 },
16677 },
16678 new_text: "".to_string(),
16679 }]),
16680 ..Default::default()
16681 };
16682
16683 let closure_completion_item = completion_item.clone();
16684 let counter = Arc::new(AtomicUsize::new(0));
16685 let counter_clone = counter.clone();
16686 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
16687 let task_completion_item = closure_completion_item.clone();
16688 counter_clone.fetch_add(1, atomic::Ordering::Release);
16689 async move {
16690 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
16691 is_incomplete: true,
16692 item_defaults: None,
16693 items: vec![task_completion_item],
16694 })))
16695 }
16696 });
16697
16698 cx.condition(|editor, _| editor.context_menu_visible())
16699 .await;
16700 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
16701 assert!(request.next().await.is_some());
16702 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
16703
16704 cx.simulate_keystrokes("S o m");
16705 cx.condition(|editor, _| editor.context_menu_visible())
16706 .await;
16707 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
16708 assert!(request.next().await.is_some());
16709 assert!(request.next().await.is_some());
16710 assert!(request.next().await.is_some());
16711 request.close();
16712 assert!(request.next().await.is_none());
16713 assert_eq!(
16714 counter.load(atomic::Ordering::Acquire),
16715 4,
16716 "With the completions menu open, only one LSP request should happen per input"
16717 );
16718}
16719
16720#[gpui::test]
16721async fn test_toggle_comment(cx: &mut TestAppContext) {
16722 init_test(cx, |_| {});
16723 let mut cx = EditorTestContext::new(cx).await;
16724 let language = Arc::new(Language::new(
16725 LanguageConfig {
16726 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16727 ..Default::default()
16728 },
16729 Some(tree_sitter_rust::LANGUAGE.into()),
16730 ));
16731 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16732
16733 // If multiple selections intersect a line, the line is only toggled once.
16734 cx.set_state(indoc! {"
16735 fn a() {
16736 «//b();
16737 ˇ»// «c();
16738 //ˇ» d();
16739 }
16740 "});
16741
16742 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16743
16744 cx.assert_editor_state(indoc! {"
16745 fn a() {
16746 «b();
16747 ˇ»«c();
16748 ˇ» d();
16749 }
16750 "});
16751
16752 // The comment prefix is inserted at the same column for every line in a
16753 // selection.
16754 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16755
16756 cx.assert_editor_state(indoc! {"
16757 fn a() {
16758 // «b();
16759 ˇ»// «c();
16760 ˇ» // d();
16761 }
16762 "});
16763
16764 // If a selection ends at the beginning of a line, that line is not toggled.
16765 cx.set_selections_state(indoc! {"
16766 fn a() {
16767 // b();
16768 «// c();
16769 ˇ» // d();
16770 }
16771 "});
16772
16773 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16774
16775 cx.assert_editor_state(indoc! {"
16776 fn a() {
16777 // b();
16778 «c();
16779 ˇ» // d();
16780 }
16781 "});
16782
16783 // If a selection span a single line and is empty, the line is toggled.
16784 cx.set_state(indoc! {"
16785 fn a() {
16786 a();
16787 b();
16788 ˇ
16789 }
16790 "});
16791
16792 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16793
16794 cx.assert_editor_state(indoc! {"
16795 fn a() {
16796 a();
16797 b();
16798 //•ˇ
16799 }
16800 "});
16801
16802 // If a selection span multiple lines, empty lines are not toggled.
16803 cx.set_state(indoc! {"
16804 fn a() {
16805 «a();
16806
16807 c();ˇ»
16808 }
16809 "});
16810
16811 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16812
16813 cx.assert_editor_state(indoc! {"
16814 fn a() {
16815 // «a();
16816
16817 // c();ˇ»
16818 }
16819 "});
16820
16821 // If a selection includes multiple comment prefixes, all lines are uncommented.
16822 cx.set_state(indoc! {"
16823 fn a() {
16824 «// a();
16825 /// b();
16826 //! c();ˇ»
16827 }
16828 "});
16829
16830 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
16831
16832 cx.assert_editor_state(indoc! {"
16833 fn a() {
16834 «a();
16835 b();
16836 c();ˇ»
16837 }
16838 "});
16839}
16840
16841#[gpui::test]
16842async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
16843 init_test(cx, |_| {});
16844 let mut cx = EditorTestContext::new(cx).await;
16845 let language = Arc::new(Language::new(
16846 LanguageConfig {
16847 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
16848 ..Default::default()
16849 },
16850 Some(tree_sitter_rust::LANGUAGE.into()),
16851 ));
16852 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
16853
16854 let toggle_comments = &ToggleComments {
16855 advance_downwards: false,
16856 ignore_indent: true,
16857 };
16858
16859 // If multiple selections intersect a line, the line is only toggled once.
16860 cx.set_state(indoc! {"
16861 fn a() {
16862 // «b();
16863 // c();
16864 // ˇ» d();
16865 }
16866 "});
16867
16868 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16869
16870 cx.assert_editor_state(indoc! {"
16871 fn a() {
16872 «b();
16873 c();
16874 ˇ» d();
16875 }
16876 "});
16877
16878 // The comment prefix is inserted at the beginning of each line
16879 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16880
16881 cx.assert_editor_state(indoc! {"
16882 fn a() {
16883 // «b();
16884 // c();
16885 // ˇ» d();
16886 }
16887 "});
16888
16889 // If a selection ends at the beginning of a line, that line is not toggled.
16890 cx.set_selections_state(indoc! {"
16891 fn a() {
16892 // b();
16893 // «c();
16894 ˇ»// d();
16895 }
16896 "});
16897
16898 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16899
16900 cx.assert_editor_state(indoc! {"
16901 fn a() {
16902 // b();
16903 «c();
16904 ˇ»// d();
16905 }
16906 "});
16907
16908 // If a selection span a single line and is empty, the line is toggled.
16909 cx.set_state(indoc! {"
16910 fn a() {
16911 a();
16912 b();
16913 ˇ
16914 }
16915 "});
16916
16917 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16918
16919 cx.assert_editor_state(indoc! {"
16920 fn a() {
16921 a();
16922 b();
16923 //ˇ
16924 }
16925 "});
16926
16927 // If a selection span multiple lines, empty lines are not toggled.
16928 cx.set_state(indoc! {"
16929 fn a() {
16930 «a();
16931
16932 c();ˇ»
16933 }
16934 "});
16935
16936 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16937
16938 cx.assert_editor_state(indoc! {"
16939 fn a() {
16940 // «a();
16941
16942 // c();ˇ»
16943 }
16944 "});
16945
16946 // If a selection includes multiple comment prefixes, all lines are uncommented.
16947 cx.set_state(indoc! {"
16948 fn a() {
16949 // «a();
16950 /// b();
16951 //! c();ˇ»
16952 }
16953 "});
16954
16955 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
16956
16957 cx.assert_editor_state(indoc! {"
16958 fn a() {
16959 «a();
16960 b();
16961 c();ˇ»
16962 }
16963 "});
16964}
16965
16966#[gpui::test]
16967async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
16968 init_test(cx, |_| {});
16969
16970 let language = Arc::new(Language::new(
16971 LanguageConfig {
16972 line_comments: vec!["// ".into()],
16973 ..Default::default()
16974 },
16975 Some(tree_sitter_rust::LANGUAGE.into()),
16976 ));
16977
16978 let mut cx = EditorTestContext::new(cx).await;
16979
16980 cx.language_registry().add(language.clone());
16981 cx.update_buffer(|buffer, cx| {
16982 buffer.set_language(Some(language), cx);
16983 });
16984
16985 let toggle_comments = &ToggleComments {
16986 advance_downwards: true,
16987 ignore_indent: false,
16988 };
16989
16990 // Single cursor on one line -> advance
16991 // Cursor moves horizontally 3 characters as well on non-blank line
16992 cx.set_state(indoc!(
16993 "fn a() {
16994 ˇdog();
16995 cat();
16996 }"
16997 ));
16998 cx.update_editor(|editor, window, cx| {
16999 editor.toggle_comments(toggle_comments, window, cx);
17000 });
17001 cx.assert_editor_state(indoc!(
17002 "fn a() {
17003 // dog();
17004 catˇ();
17005 }"
17006 ));
17007
17008 // Single selection on one line -> don't advance
17009 cx.set_state(indoc!(
17010 "fn a() {
17011 «dog()ˇ»;
17012 cat();
17013 }"
17014 ));
17015 cx.update_editor(|editor, window, cx| {
17016 editor.toggle_comments(toggle_comments, window, cx);
17017 });
17018 cx.assert_editor_state(indoc!(
17019 "fn a() {
17020 // «dog()ˇ»;
17021 cat();
17022 }"
17023 ));
17024
17025 // Multiple cursors on one line -> advance
17026 cx.set_state(indoc!(
17027 "fn a() {
17028 ˇdˇog();
17029 cat();
17030 }"
17031 ));
17032 cx.update_editor(|editor, window, cx| {
17033 editor.toggle_comments(toggle_comments, window, cx);
17034 });
17035 cx.assert_editor_state(indoc!(
17036 "fn a() {
17037 // dog();
17038 catˇ(ˇ);
17039 }"
17040 ));
17041
17042 // Multiple cursors on one line, with selection -> don't advance
17043 cx.set_state(indoc!(
17044 "fn a() {
17045 ˇdˇog«()ˇ»;
17046 cat();
17047 }"
17048 ));
17049 cx.update_editor(|editor, window, cx| {
17050 editor.toggle_comments(toggle_comments, window, cx);
17051 });
17052 cx.assert_editor_state(indoc!(
17053 "fn a() {
17054 // ˇdˇog«()ˇ»;
17055 cat();
17056 }"
17057 ));
17058
17059 // Single cursor on one line -> advance
17060 // Cursor moves to column 0 on blank line
17061 cx.set_state(indoc!(
17062 "fn a() {
17063 ˇdog();
17064
17065 cat();
17066 }"
17067 ));
17068 cx.update_editor(|editor, window, cx| {
17069 editor.toggle_comments(toggle_comments, window, cx);
17070 });
17071 cx.assert_editor_state(indoc!(
17072 "fn a() {
17073 // dog();
17074 ˇ
17075 cat();
17076 }"
17077 ));
17078
17079 // Single cursor on one line -> advance
17080 // Cursor starts and ends at column 0
17081 cx.set_state(indoc!(
17082 "fn a() {
17083 ˇ dog();
17084 cat();
17085 }"
17086 ));
17087 cx.update_editor(|editor, window, cx| {
17088 editor.toggle_comments(toggle_comments, window, cx);
17089 });
17090 cx.assert_editor_state(indoc!(
17091 "fn a() {
17092 // dog();
17093 ˇ cat();
17094 }"
17095 ));
17096}
17097
17098#[gpui::test]
17099async fn test_toggle_block_comment(cx: &mut TestAppContext) {
17100 init_test(cx, |_| {});
17101
17102 let mut cx = EditorTestContext::new(cx).await;
17103
17104 let html_language = Arc::new(
17105 Language::new(
17106 LanguageConfig {
17107 name: "HTML".into(),
17108 block_comment: Some(BlockCommentConfig {
17109 start: "<!-- ".into(),
17110 prefix: "".into(),
17111 end: " -->".into(),
17112 tab_size: 0,
17113 }),
17114 ..Default::default()
17115 },
17116 Some(tree_sitter_html::LANGUAGE.into()),
17117 )
17118 .with_injection_query(
17119 r#"
17120 (script_element
17121 (raw_text) @injection.content
17122 (#set! injection.language "javascript"))
17123 "#,
17124 )
17125 .unwrap(),
17126 );
17127
17128 let javascript_language = Arc::new(Language::new(
17129 LanguageConfig {
17130 name: "JavaScript".into(),
17131 line_comments: vec!["// ".into()],
17132 ..Default::default()
17133 },
17134 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17135 ));
17136
17137 cx.language_registry().add(html_language.clone());
17138 cx.language_registry().add(javascript_language);
17139 cx.update_buffer(|buffer, cx| {
17140 buffer.set_language(Some(html_language), cx);
17141 });
17142
17143 // Toggle comments for empty selections
17144 cx.set_state(
17145 &r#"
17146 <p>A</p>ˇ
17147 <p>B</p>ˇ
17148 <p>C</p>ˇ
17149 "#
17150 .unindent(),
17151 );
17152 cx.update_editor(|editor, window, cx| {
17153 editor.toggle_comments(&ToggleComments::default(), window, cx)
17154 });
17155 cx.assert_editor_state(
17156 &r#"
17157 <!-- <p>A</p>ˇ -->
17158 <!-- <p>B</p>ˇ -->
17159 <!-- <p>C</p>ˇ -->
17160 "#
17161 .unindent(),
17162 );
17163 cx.update_editor(|editor, window, cx| {
17164 editor.toggle_comments(&ToggleComments::default(), window, cx)
17165 });
17166 cx.assert_editor_state(
17167 &r#"
17168 <p>A</p>ˇ
17169 <p>B</p>ˇ
17170 <p>C</p>ˇ
17171 "#
17172 .unindent(),
17173 );
17174
17175 // Toggle comments for mixture of empty and non-empty selections, where
17176 // multiple selections occupy a given line.
17177 cx.set_state(
17178 &r#"
17179 <p>A«</p>
17180 <p>ˇ»B</p>ˇ
17181 <p>C«</p>
17182 <p>ˇ»D</p>ˇ
17183 "#
17184 .unindent(),
17185 );
17186
17187 cx.update_editor(|editor, window, cx| {
17188 editor.toggle_comments(&ToggleComments::default(), window, cx)
17189 });
17190 cx.assert_editor_state(
17191 &r#"
17192 <!-- <p>A«</p>
17193 <p>ˇ»B</p>ˇ -->
17194 <!-- <p>C«</p>
17195 <p>ˇ»D</p>ˇ -->
17196 "#
17197 .unindent(),
17198 );
17199 cx.update_editor(|editor, window, cx| {
17200 editor.toggle_comments(&ToggleComments::default(), window, cx)
17201 });
17202 cx.assert_editor_state(
17203 &r#"
17204 <p>A«</p>
17205 <p>ˇ»B</p>ˇ
17206 <p>C«</p>
17207 <p>ˇ»D</p>ˇ
17208 "#
17209 .unindent(),
17210 );
17211
17212 // Toggle comments when different languages are active for different
17213 // selections.
17214 cx.set_state(
17215 &r#"
17216 ˇ<script>
17217 ˇvar x = new Y();
17218 ˇ</script>
17219 "#
17220 .unindent(),
17221 );
17222 cx.executor().run_until_parked();
17223 cx.update_editor(|editor, window, cx| {
17224 editor.toggle_comments(&ToggleComments::default(), window, cx)
17225 });
17226 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
17227 // Uncommenting and commenting from this position brings in even more wrong artifacts.
17228 cx.assert_editor_state(
17229 &r#"
17230 <!-- ˇ<script> -->
17231 // ˇvar x = new Y();
17232 <!-- ˇ</script> -->
17233 "#
17234 .unindent(),
17235 );
17236}
17237
17238#[gpui::test]
17239fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
17240 init_test(cx, |_| {});
17241
17242 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
17243 let multibuffer = cx.new(|cx| {
17244 let mut multibuffer = MultiBuffer::new(ReadWrite);
17245 multibuffer.push_excerpts(
17246 buffer.clone(),
17247 [
17248 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
17249 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
17250 ],
17251 cx,
17252 );
17253 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
17254 multibuffer
17255 });
17256
17257 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
17258 editor.update_in(cx, |editor, window, cx| {
17259 assert_eq!(editor.text(cx), "aaaa\nbbbb");
17260 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17261 s.select_ranges([
17262 Point::new(0, 0)..Point::new(0, 0),
17263 Point::new(1, 0)..Point::new(1, 0),
17264 ])
17265 });
17266
17267 editor.handle_input("X", window, cx);
17268 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
17269 assert_eq!(
17270 editor.selections.ranges(&editor.display_snapshot(cx)),
17271 [
17272 Point::new(0, 1)..Point::new(0, 1),
17273 Point::new(1, 1)..Point::new(1, 1),
17274 ]
17275 );
17276
17277 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
17278 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17279 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
17280 });
17281 editor.backspace(&Default::default(), window, cx);
17282 assert_eq!(editor.text(cx), "Xa\nbbb");
17283 assert_eq!(
17284 editor.selections.ranges(&editor.display_snapshot(cx)),
17285 [Point::new(1, 0)..Point::new(1, 0)]
17286 );
17287
17288 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17289 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
17290 });
17291 editor.backspace(&Default::default(), window, cx);
17292 assert_eq!(editor.text(cx), "X\nbb");
17293 assert_eq!(
17294 editor.selections.ranges(&editor.display_snapshot(cx)),
17295 [Point::new(0, 1)..Point::new(0, 1)]
17296 );
17297 });
17298}
17299
17300#[gpui::test]
17301fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
17302 init_test(cx, |_| {});
17303
17304 let markers = vec![('[', ']').into(), ('(', ')').into()];
17305 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
17306 indoc! {"
17307 [aaaa
17308 (bbbb]
17309 cccc)",
17310 },
17311 markers.clone(),
17312 );
17313 let excerpt_ranges = markers.into_iter().map(|marker| {
17314 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
17315 ExcerptRange::new(context)
17316 });
17317 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
17318 let multibuffer = cx.new(|cx| {
17319 let mut multibuffer = MultiBuffer::new(ReadWrite);
17320 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
17321 multibuffer
17322 });
17323
17324 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
17325 editor.update_in(cx, |editor, window, cx| {
17326 let (expected_text, selection_ranges) = marked_text_ranges(
17327 indoc! {"
17328 aaaa
17329 bˇbbb
17330 bˇbbˇb
17331 cccc"
17332 },
17333 true,
17334 );
17335 assert_eq!(editor.text(cx), expected_text);
17336 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17337 s.select_ranges(
17338 selection_ranges
17339 .iter()
17340 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)),
17341 )
17342 });
17343
17344 editor.handle_input("X", window, cx);
17345
17346 let (expected_text, expected_selections) = marked_text_ranges(
17347 indoc! {"
17348 aaaa
17349 bXˇbbXb
17350 bXˇbbXˇb
17351 cccc"
17352 },
17353 false,
17354 );
17355 assert_eq!(editor.text(cx), expected_text);
17356 assert_eq!(
17357 editor.selections.ranges(&editor.display_snapshot(cx)),
17358 expected_selections
17359 .iter()
17360 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
17361 .collect::<Vec<_>>()
17362 );
17363
17364 editor.newline(&Newline, window, cx);
17365 let (expected_text, expected_selections) = marked_text_ranges(
17366 indoc! {"
17367 aaaa
17368 bX
17369 ˇbbX
17370 b
17371 bX
17372 ˇbbX
17373 ˇb
17374 cccc"
17375 },
17376 false,
17377 );
17378 assert_eq!(editor.text(cx), expected_text);
17379 assert_eq!(
17380 editor.selections.ranges(&editor.display_snapshot(cx)),
17381 expected_selections
17382 .iter()
17383 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
17384 .collect::<Vec<_>>()
17385 );
17386 });
17387}
17388
17389#[gpui::test]
17390fn test_refresh_selections(cx: &mut TestAppContext) {
17391 init_test(cx, |_| {});
17392
17393 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
17394 let mut excerpt1_id = None;
17395 let multibuffer = cx.new(|cx| {
17396 let mut multibuffer = MultiBuffer::new(ReadWrite);
17397 excerpt1_id = multibuffer
17398 .push_excerpts(
17399 buffer.clone(),
17400 [
17401 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
17402 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
17403 ],
17404 cx,
17405 )
17406 .into_iter()
17407 .next();
17408 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
17409 multibuffer
17410 });
17411
17412 let editor = cx.add_window(|window, cx| {
17413 let mut editor = build_editor(multibuffer.clone(), window, cx);
17414 let snapshot = editor.snapshot(window, cx);
17415 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17416 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
17417 });
17418 editor.begin_selection(
17419 Point::new(2, 1).to_display_point(&snapshot),
17420 true,
17421 1,
17422 window,
17423 cx,
17424 );
17425 assert_eq!(
17426 editor.selections.ranges(&editor.display_snapshot(cx)),
17427 [
17428 Point::new(1, 3)..Point::new(1, 3),
17429 Point::new(2, 1)..Point::new(2, 1),
17430 ]
17431 );
17432 editor
17433 });
17434
17435 // Refreshing selections is a no-op when excerpts haven't changed.
17436 _ = editor.update(cx, |editor, window, cx| {
17437 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17438 assert_eq!(
17439 editor.selections.ranges(&editor.display_snapshot(cx)),
17440 [
17441 Point::new(1, 3)..Point::new(1, 3),
17442 Point::new(2, 1)..Point::new(2, 1),
17443 ]
17444 );
17445 });
17446
17447 multibuffer.update(cx, |multibuffer, cx| {
17448 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
17449 });
17450 _ = editor.update(cx, |editor, window, cx| {
17451 // Removing an excerpt causes the first selection to become degenerate.
17452 assert_eq!(
17453 editor.selections.ranges(&editor.display_snapshot(cx)),
17454 [
17455 Point::new(0, 0)..Point::new(0, 0),
17456 Point::new(0, 1)..Point::new(0, 1)
17457 ]
17458 );
17459
17460 // Refreshing selections will relocate the first selection to the original buffer
17461 // location.
17462 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17463 assert_eq!(
17464 editor.selections.ranges(&editor.display_snapshot(cx)),
17465 [
17466 Point::new(0, 1)..Point::new(0, 1),
17467 Point::new(0, 3)..Point::new(0, 3)
17468 ]
17469 );
17470 assert!(editor.selections.pending_anchor().is_some());
17471 });
17472}
17473
17474#[gpui::test]
17475fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
17476 init_test(cx, |_| {});
17477
17478 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
17479 let mut excerpt1_id = None;
17480 let multibuffer = cx.new(|cx| {
17481 let mut multibuffer = MultiBuffer::new(ReadWrite);
17482 excerpt1_id = multibuffer
17483 .push_excerpts(
17484 buffer.clone(),
17485 [
17486 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
17487 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
17488 ],
17489 cx,
17490 )
17491 .into_iter()
17492 .next();
17493 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
17494 multibuffer
17495 });
17496
17497 let editor = cx.add_window(|window, cx| {
17498 let mut editor = build_editor(multibuffer.clone(), window, cx);
17499 let snapshot = editor.snapshot(window, cx);
17500 editor.begin_selection(
17501 Point::new(1, 3).to_display_point(&snapshot),
17502 false,
17503 1,
17504 window,
17505 cx,
17506 );
17507 assert_eq!(
17508 editor.selections.ranges(&editor.display_snapshot(cx)),
17509 [Point::new(1, 3)..Point::new(1, 3)]
17510 );
17511 editor
17512 });
17513
17514 multibuffer.update(cx, |multibuffer, cx| {
17515 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
17516 });
17517 _ = editor.update(cx, |editor, window, cx| {
17518 assert_eq!(
17519 editor.selections.ranges(&editor.display_snapshot(cx)),
17520 [Point::new(0, 0)..Point::new(0, 0)]
17521 );
17522
17523 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
17524 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
17525 assert_eq!(
17526 editor.selections.ranges(&editor.display_snapshot(cx)),
17527 [Point::new(0, 3)..Point::new(0, 3)]
17528 );
17529 assert!(editor.selections.pending_anchor().is_some());
17530 });
17531}
17532
17533#[gpui::test]
17534async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
17535 init_test(cx, |_| {});
17536
17537 let language = Arc::new(
17538 Language::new(
17539 LanguageConfig {
17540 brackets: BracketPairConfig {
17541 pairs: vec![
17542 BracketPair {
17543 start: "{".to_string(),
17544 end: "}".to_string(),
17545 close: true,
17546 surround: true,
17547 newline: true,
17548 },
17549 BracketPair {
17550 start: "/* ".to_string(),
17551 end: " */".to_string(),
17552 close: true,
17553 surround: true,
17554 newline: true,
17555 },
17556 ],
17557 ..Default::default()
17558 },
17559 ..Default::default()
17560 },
17561 Some(tree_sitter_rust::LANGUAGE.into()),
17562 )
17563 .with_indents_query("")
17564 .unwrap(),
17565 );
17566
17567 let text = concat!(
17568 "{ }\n", //
17569 " x\n", //
17570 " /* */\n", //
17571 "x\n", //
17572 "{{} }\n", //
17573 );
17574
17575 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17576 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
17577 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
17578 editor
17579 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
17580 .await;
17581
17582 editor.update_in(cx, |editor, window, cx| {
17583 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17584 s.select_display_ranges([
17585 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
17586 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
17587 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
17588 ])
17589 });
17590 editor.newline(&Newline, window, cx);
17591
17592 assert_eq!(
17593 editor.buffer().read(cx).read(cx).text(),
17594 concat!(
17595 "{ \n", // Suppress rustfmt
17596 "\n", //
17597 "}\n", //
17598 " x\n", //
17599 " /* \n", //
17600 " \n", //
17601 " */\n", //
17602 "x\n", //
17603 "{{} \n", //
17604 "}\n", //
17605 )
17606 );
17607 });
17608}
17609
17610#[gpui::test]
17611fn test_highlighted_ranges(cx: &mut TestAppContext) {
17612 init_test(cx, |_| {});
17613
17614 let editor = cx.add_window(|window, cx| {
17615 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
17616 build_editor(buffer, window, cx)
17617 });
17618
17619 _ = editor.update(cx, |editor, window, cx| {
17620 let buffer = editor.buffer.read(cx).snapshot(cx);
17621
17622 let anchor_range =
17623 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
17624
17625 editor.highlight_background(
17626 HighlightKey::ColorizeBracket(0),
17627 &[
17628 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
17629 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
17630 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
17631 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
17632 ],
17633 |_, _| Hsla::red(),
17634 cx,
17635 );
17636 editor.highlight_background(
17637 HighlightKey::ColorizeBracket(1),
17638 &[
17639 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
17640 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
17641 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
17642 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
17643 ],
17644 |_, _| Hsla::green(),
17645 cx,
17646 );
17647
17648 let snapshot = editor.snapshot(window, cx);
17649 let highlighted_ranges = editor.sorted_background_highlights_in_range(
17650 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
17651 &snapshot,
17652 cx.theme(),
17653 );
17654 assert_eq!(
17655 highlighted_ranges,
17656 &[
17657 (
17658 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
17659 Hsla::green(),
17660 ),
17661 (
17662 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
17663 Hsla::red(),
17664 ),
17665 (
17666 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
17667 Hsla::green(),
17668 ),
17669 (
17670 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17671 Hsla::red(),
17672 ),
17673 ]
17674 );
17675 assert_eq!(
17676 editor.sorted_background_highlights_in_range(
17677 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
17678 &snapshot,
17679 cx.theme(),
17680 ),
17681 &[(
17682 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
17683 Hsla::red(),
17684 )]
17685 );
17686 });
17687}
17688
17689#[gpui::test]
17690async fn test_following(cx: &mut TestAppContext) {
17691 init_test(cx, |_| {});
17692
17693 let fs = FakeFs::new(cx.executor());
17694 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17695
17696 let buffer = project.update(cx, |project, cx| {
17697 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
17698 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
17699 });
17700 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
17701 let follower = cx.update(|cx| {
17702 cx.open_window(
17703 WindowOptions {
17704 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
17705 gpui::Point::new(px(0.), px(0.)),
17706 gpui::Point::new(px(10.), px(80.)),
17707 ))),
17708 ..Default::default()
17709 },
17710 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
17711 )
17712 .unwrap()
17713 });
17714
17715 let is_still_following = Rc::new(RefCell::new(true));
17716 let follower_edit_event_count = Rc::new(RefCell::new(0));
17717 let pending_update = Rc::new(RefCell::new(None));
17718 let leader_entity = leader.root(cx).unwrap();
17719 let follower_entity = follower.root(cx).unwrap();
17720 _ = follower.update(cx, {
17721 let update = pending_update.clone();
17722 let is_still_following = is_still_following.clone();
17723 let follower_edit_event_count = follower_edit_event_count.clone();
17724 |_, window, cx| {
17725 cx.subscribe_in(
17726 &leader_entity,
17727 window,
17728 move |_, leader, event, window, cx| {
17729 leader.update(cx, |leader, cx| {
17730 leader.add_event_to_update_proto(
17731 event,
17732 &mut update.borrow_mut(),
17733 window,
17734 cx,
17735 );
17736 });
17737 },
17738 )
17739 .detach();
17740
17741 cx.subscribe_in(
17742 &follower_entity,
17743 window,
17744 move |_, _, event: &EditorEvent, _window, _cx| {
17745 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
17746 *is_still_following.borrow_mut() = false;
17747 }
17748
17749 if let EditorEvent::BufferEdited = event {
17750 *follower_edit_event_count.borrow_mut() += 1;
17751 }
17752 },
17753 )
17754 .detach();
17755 }
17756 });
17757
17758 // Update the selections only
17759 _ = leader.update(cx, |leader, window, cx| {
17760 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17761 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17762 });
17763 });
17764 follower
17765 .update(cx, |follower, window, cx| {
17766 follower.apply_update_proto(
17767 &project,
17768 pending_update.borrow_mut().take().unwrap(),
17769 window,
17770 cx,
17771 )
17772 })
17773 .unwrap()
17774 .await
17775 .unwrap();
17776 _ = follower.update(cx, |follower, _, cx| {
17777 assert_eq!(
17778 follower.selections.ranges(&follower.display_snapshot(cx)),
17779 vec![MultiBufferOffset(1)..MultiBufferOffset(1)]
17780 );
17781 });
17782 assert!(*is_still_following.borrow());
17783 assert_eq!(*follower_edit_event_count.borrow(), 0);
17784
17785 // Update the scroll position only
17786 _ = leader.update(cx, |leader, window, cx| {
17787 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17788 });
17789 follower
17790 .update(cx, |follower, window, cx| {
17791 follower.apply_update_proto(
17792 &project,
17793 pending_update.borrow_mut().take().unwrap(),
17794 window,
17795 cx,
17796 )
17797 })
17798 .unwrap()
17799 .await
17800 .unwrap();
17801 assert_eq!(
17802 follower
17803 .update(cx, |follower, _, cx| follower.scroll_position(cx))
17804 .unwrap(),
17805 gpui::Point::new(1.5, 3.5)
17806 );
17807 assert!(*is_still_following.borrow());
17808 assert_eq!(*follower_edit_event_count.borrow(), 0);
17809
17810 // Update the selections and scroll position. The follower's scroll position is updated
17811 // via autoscroll, not via the leader's exact scroll position.
17812 _ = leader.update(cx, |leader, window, cx| {
17813 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17814 s.select_ranges([MultiBufferOffset(0)..MultiBufferOffset(0)])
17815 });
17816 leader.request_autoscroll(Autoscroll::newest(), cx);
17817 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
17818 });
17819 follower
17820 .update(cx, |follower, window, cx| {
17821 follower.apply_update_proto(
17822 &project,
17823 pending_update.borrow_mut().take().unwrap(),
17824 window,
17825 cx,
17826 )
17827 })
17828 .unwrap()
17829 .await
17830 .unwrap();
17831 _ = follower.update(cx, |follower, _, cx| {
17832 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
17833 assert_eq!(
17834 follower.selections.ranges(&follower.display_snapshot(cx)),
17835 vec![MultiBufferOffset(0)..MultiBufferOffset(0)]
17836 );
17837 });
17838 assert!(*is_still_following.borrow());
17839
17840 // Creating a pending selection that precedes another selection
17841 _ = leader.update(cx, |leader, window, cx| {
17842 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17843 s.select_ranges([MultiBufferOffset(1)..MultiBufferOffset(1)])
17844 });
17845 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
17846 });
17847 follower
17848 .update(cx, |follower, window, cx| {
17849 follower.apply_update_proto(
17850 &project,
17851 pending_update.borrow_mut().take().unwrap(),
17852 window,
17853 cx,
17854 )
17855 })
17856 .unwrap()
17857 .await
17858 .unwrap();
17859 _ = follower.update(cx, |follower, _, cx| {
17860 assert_eq!(
17861 follower.selections.ranges(&follower.display_snapshot(cx)),
17862 vec![
17863 MultiBufferOffset(0)..MultiBufferOffset(0),
17864 MultiBufferOffset(1)..MultiBufferOffset(1)
17865 ]
17866 );
17867 });
17868 assert!(*is_still_following.borrow());
17869
17870 // Extend the pending selection so that it surrounds another selection
17871 _ = leader.update(cx, |leader, window, cx| {
17872 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
17873 });
17874 follower
17875 .update(cx, |follower, window, cx| {
17876 follower.apply_update_proto(
17877 &project,
17878 pending_update.borrow_mut().take().unwrap(),
17879 window,
17880 cx,
17881 )
17882 })
17883 .unwrap()
17884 .await
17885 .unwrap();
17886 _ = follower.update(cx, |follower, _, cx| {
17887 assert_eq!(
17888 follower.selections.ranges(&follower.display_snapshot(cx)),
17889 vec![MultiBufferOffset(0)..MultiBufferOffset(2)]
17890 );
17891 });
17892
17893 // Scrolling locally breaks the follow
17894 _ = follower.update(cx, |follower, window, cx| {
17895 let top_anchor = follower
17896 .buffer()
17897 .read(cx)
17898 .read(cx)
17899 .anchor_after(MultiBufferOffset(0));
17900 follower.set_scroll_anchor(
17901 ScrollAnchor {
17902 anchor: top_anchor,
17903 offset: gpui::Point::new(0.0, 0.5),
17904 },
17905 window,
17906 cx,
17907 );
17908 });
17909 assert!(!(*is_still_following.borrow()));
17910}
17911
17912#[gpui::test]
17913async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
17914 init_test(cx, |_| {});
17915
17916 let fs = FakeFs::new(cx.executor());
17917 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
17918 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17919 let pane = workspace
17920 .update(cx, |workspace, _, _| workspace.active_pane().clone())
17921 .unwrap();
17922
17923 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17924
17925 let leader = pane.update_in(cx, |_, window, cx| {
17926 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
17927 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
17928 });
17929
17930 // Start following the editor when it has no excerpts.
17931 let mut state_message =
17932 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
17933 let workspace_entity = workspace.root(cx).unwrap();
17934 let follower_1 = cx
17935 .update_window(*workspace.deref(), |_, window, cx| {
17936 Editor::from_state_proto(
17937 workspace_entity,
17938 ViewId {
17939 creator: CollaboratorId::PeerId(PeerId::default()),
17940 id: 0,
17941 },
17942 &mut state_message,
17943 window,
17944 cx,
17945 )
17946 })
17947 .unwrap()
17948 .unwrap()
17949 .await
17950 .unwrap();
17951
17952 let update_message = Rc::new(RefCell::new(None));
17953 follower_1.update_in(cx, {
17954 let update = update_message.clone();
17955 |_, window, cx| {
17956 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
17957 leader.update(cx, |leader, cx| {
17958 leader.add_event_to_update_proto(event, &mut update.borrow_mut(), window, cx);
17959 });
17960 })
17961 .detach();
17962 }
17963 });
17964
17965 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
17966 (
17967 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
17968 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
17969 )
17970 });
17971
17972 // Insert some excerpts.
17973 leader.update(cx, |leader, cx| {
17974 leader.buffer.update(cx, |multibuffer, cx| {
17975 multibuffer.set_excerpts_for_path(
17976 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
17977 buffer_1.clone(),
17978 vec![
17979 Point::row_range(0..3),
17980 Point::row_range(1..6),
17981 Point::row_range(12..15),
17982 ],
17983 0,
17984 cx,
17985 );
17986 multibuffer.set_excerpts_for_path(
17987 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
17988 buffer_2.clone(),
17989 vec![Point::row_range(0..6), Point::row_range(8..12)],
17990 0,
17991 cx,
17992 );
17993 });
17994 });
17995
17996 // Apply the update of adding the excerpts.
17997 follower_1
17998 .update_in(cx, |follower, window, cx| {
17999 follower.apply_update_proto(
18000 &project,
18001 update_message.borrow().clone().unwrap(),
18002 window,
18003 cx,
18004 )
18005 })
18006 .await
18007 .unwrap();
18008 assert_eq!(
18009 follower_1.update(cx, |editor, cx| editor.text(cx)),
18010 leader.update(cx, |editor, cx| editor.text(cx))
18011 );
18012 update_message.borrow_mut().take();
18013
18014 // Start following separately after it already has excerpts.
18015 let mut state_message =
18016 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
18017 let workspace_entity = workspace.root(cx).unwrap();
18018 let follower_2 = cx
18019 .update_window(*workspace.deref(), |_, window, cx| {
18020 Editor::from_state_proto(
18021 workspace_entity,
18022 ViewId {
18023 creator: CollaboratorId::PeerId(PeerId::default()),
18024 id: 0,
18025 },
18026 &mut state_message,
18027 window,
18028 cx,
18029 )
18030 })
18031 .unwrap()
18032 .unwrap()
18033 .await
18034 .unwrap();
18035 assert_eq!(
18036 follower_2.update(cx, |editor, cx| editor.text(cx)),
18037 leader.update(cx, |editor, cx| editor.text(cx))
18038 );
18039
18040 // Remove some excerpts.
18041 leader.update(cx, |leader, cx| {
18042 leader.buffer.update(cx, |multibuffer, cx| {
18043 let excerpt_ids = multibuffer.excerpt_ids();
18044 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
18045 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
18046 });
18047 });
18048
18049 // Apply the update of removing the excerpts.
18050 follower_1
18051 .update_in(cx, |follower, window, cx| {
18052 follower.apply_update_proto(
18053 &project,
18054 update_message.borrow().clone().unwrap(),
18055 window,
18056 cx,
18057 )
18058 })
18059 .await
18060 .unwrap();
18061 follower_2
18062 .update_in(cx, |follower, window, cx| {
18063 follower.apply_update_proto(
18064 &project,
18065 update_message.borrow().clone().unwrap(),
18066 window,
18067 cx,
18068 )
18069 })
18070 .await
18071 .unwrap();
18072 update_message.borrow_mut().take();
18073 assert_eq!(
18074 follower_1.update(cx, |editor, cx| editor.text(cx)),
18075 leader.update(cx, |editor, cx| editor.text(cx))
18076 );
18077}
18078
18079#[gpui::test]
18080async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18081 init_test(cx, |_| {});
18082
18083 let mut cx = EditorTestContext::new(cx).await;
18084 let lsp_store =
18085 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
18086
18087 cx.set_state(indoc! {"
18088 ˇfn func(abc def: i32) -> u32 {
18089 }
18090 "});
18091
18092 cx.update(|_, cx| {
18093 lsp_store.update(cx, |lsp_store, cx| {
18094 lsp_store
18095 .update_diagnostics(
18096 LanguageServerId(0),
18097 lsp::PublishDiagnosticsParams {
18098 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
18099 version: None,
18100 diagnostics: vec![
18101 lsp::Diagnostic {
18102 range: lsp::Range::new(
18103 lsp::Position::new(0, 11),
18104 lsp::Position::new(0, 12),
18105 ),
18106 severity: Some(lsp::DiagnosticSeverity::ERROR),
18107 ..Default::default()
18108 },
18109 lsp::Diagnostic {
18110 range: lsp::Range::new(
18111 lsp::Position::new(0, 12),
18112 lsp::Position::new(0, 15),
18113 ),
18114 severity: Some(lsp::DiagnosticSeverity::ERROR),
18115 ..Default::default()
18116 },
18117 lsp::Diagnostic {
18118 range: lsp::Range::new(
18119 lsp::Position::new(0, 25),
18120 lsp::Position::new(0, 28),
18121 ),
18122 severity: Some(lsp::DiagnosticSeverity::ERROR),
18123 ..Default::default()
18124 },
18125 ],
18126 },
18127 None,
18128 DiagnosticSourceKind::Pushed,
18129 &[],
18130 cx,
18131 )
18132 .unwrap()
18133 });
18134 });
18135
18136 executor.run_until_parked();
18137
18138 cx.update_editor(|editor, window, cx| {
18139 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
18140 });
18141
18142 cx.assert_editor_state(indoc! {"
18143 fn func(abc def: i32) -> ˇu32 {
18144 }
18145 "});
18146
18147 cx.update_editor(|editor, window, cx| {
18148 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
18149 });
18150
18151 cx.assert_editor_state(indoc! {"
18152 fn func(abc ˇdef: i32) -> u32 {
18153 }
18154 "});
18155
18156 cx.update_editor(|editor, window, cx| {
18157 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
18158 });
18159
18160 cx.assert_editor_state(indoc! {"
18161 fn func(abcˇ def: i32) -> u32 {
18162 }
18163 "});
18164
18165 cx.update_editor(|editor, window, cx| {
18166 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
18167 });
18168
18169 cx.assert_editor_state(indoc! {"
18170 fn func(abc def: i32) -> ˇu32 {
18171 }
18172 "});
18173}
18174
18175#[gpui::test]
18176async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18177 init_test(cx, |_| {});
18178
18179 let mut cx = EditorTestContext::new(cx).await;
18180
18181 let diff_base = r#"
18182 use some::mod;
18183
18184 const A: u32 = 42;
18185
18186 fn main() {
18187 println!("hello");
18188
18189 println!("world");
18190 }
18191 "#
18192 .unindent();
18193
18194 // Edits are modified, removed, modified, added
18195 cx.set_state(
18196 &r#"
18197 use some::modified;
18198
18199 ˇ
18200 fn main() {
18201 println!("hello there");
18202
18203 println!("around the");
18204 println!("world");
18205 }
18206 "#
18207 .unindent(),
18208 );
18209
18210 cx.set_head_text(&diff_base);
18211 executor.run_until_parked();
18212
18213 cx.update_editor(|editor, window, cx| {
18214 //Wrap around the bottom of the buffer
18215 for _ in 0..3 {
18216 editor.go_to_next_hunk(&GoToHunk, window, cx);
18217 }
18218 });
18219
18220 cx.assert_editor_state(
18221 &r#"
18222 ˇuse some::modified;
18223
18224
18225 fn main() {
18226 println!("hello there");
18227
18228 println!("around the");
18229 println!("world");
18230 }
18231 "#
18232 .unindent(),
18233 );
18234
18235 cx.update_editor(|editor, window, cx| {
18236 //Wrap around the top of the buffer
18237 for _ in 0..2 {
18238 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
18239 }
18240 });
18241
18242 cx.assert_editor_state(
18243 &r#"
18244 use some::modified;
18245
18246
18247 fn main() {
18248 ˇ println!("hello there");
18249
18250 println!("around the");
18251 println!("world");
18252 }
18253 "#
18254 .unindent(),
18255 );
18256
18257 cx.update_editor(|editor, window, cx| {
18258 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
18259 });
18260
18261 cx.assert_editor_state(
18262 &r#"
18263 use some::modified;
18264
18265 ˇ
18266 fn main() {
18267 println!("hello there");
18268
18269 println!("around the");
18270 println!("world");
18271 }
18272 "#
18273 .unindent(),
18274 );
18275
18276 cx.update_editor(|editor, window, cx| {
18277 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
18278 });
18279
18280 cx.assert_editor_state(
18281 &r#"
18282 ˇuse some::modified;
18283
18284
18285 fn main() {
18286 println!("hello there");
18287
18288 println!("around the");
18289 println!("world");
18290 }
18291 "#
18292 .unindent(),
18293 );
18294
18295 cx.update_editor(|editor, window, cx| {
18296 for _ in 0..2 {
18297 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
18298 }
18299 });
18300
18301 cx.assert_editor_state(
18302 &r#"
18303 use some::modified;
18304
18305
18306 fn main() {
18307 ˇ println!("hello there");
18308
18309 println!("around the");
18310 println!("world");
18311 }
18312 "#
18313 .unindent(),
18314 );
18315
18316 cx.update_editor(|editor, window, cx| {
18317 editor.fold(&Fold, window, cx);
18318 });
18319
18320 cx.update_editor(|editor, window, cx| {
18321 editor.go_to_next_hunk(&GoToHunk, window, cx);
18322 });
18323
18324 cx.assert_editor_state(
18325 &r#"
18326 ˇuse some::modified;
18327
18328
18329 fn main() {
18330 println!("hello there");
18331
18332 println!("around the");
18333 println!("world");
18334 }
18335 "#
18336 .unindent(),
18337 );
18338}
18339
18340#[test]
18341fn test_split_words() {
18342 fn split(text: &str) -> Vec<&str> {
18343 split_words(text).collect()
18344 }
18345
18346 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
18347 assert_eq!(split("hello_world"), &["hello_", "world"]);
18348 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
18349 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
18350 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
18351 assert_eq!(split("helloworld"), &["helloworld"]);
18352
18353 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
18354}
18355
18356#[test]
18357fn test_split_words_for_snippet_prefix() {
18358 fn split(text: &str) -> Vec<&str> {
18359 snippet_candidate_suffixes(text, |c| c.is_alphanumeric() || c == '_').collect()
18360 }
18361
18362 assert_eq!(split("HelloWorld"), &["HelloWorld"]);
18363 assert_eq!(split("hello_world"), &["hello_world"]);
18364 assert_eq!(split("_hello_world_"), &["_hello_world_"]);
18365 assert_eq!(split("Hello_World"), &["Hello_World"]);
18366 assert_eq!(split("helloWOrld"), &["helloWOrld"]);
18367 assert_eq!(split("helloworld"), &["helloworld"]);
18368 assert_eq!(
18369 split("this@is!@#$^many . symbols"),
18370 &[
18371 "symbols",
18372 " symbols",
18373 ". symbols",
18374 " . symbols",
18375 " . symbols",
18376 " . symbols",
18377 "many . symbols",
18378 "^many . symbols",
18379 "$^many . symbols",
18380 "#$^many . symbols",
18381 "@#$^many . symbols",
18382 "!@#$^many . symbols",
18383 "is!@#$^many . symbols",
18384 "@is!@#$^many . symbols",
18385 "this@is!@#$^many . symbols",
18386 ],
18387 );
18388 assert_eq!(split("a.s"), &["s", ".s", "a.s"]);
18389}
18390
18391#[gpui::test]
18392async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
18393 init_test(cx, |_| {});
18394
18395 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
18396
18397 #[track_caller]
18398 fn assert(before: &str, after: &str, cx: &mut EditorLspTestContext) {
18399 let _state_context = cx.set_state(before);
18400 cx.run_until_parked();
18401 cx.update_editor(|editor, window, cx| {
18402 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
18403 });
18404 cx.run_until_parked();
18405 cx.assert_editor_state(after);
18406 }
18407
18408 // Outside bracket jumps to outside of matching bracket
18409 assert("console.logˇ(var);", "console.log(var)ˇ;", &mut cx);
18410 assert("console.log(var)ˇ;", "console.logˇ(var);", &mut cx);
18411
18412 // Inside bracket jumps to inside of matching bracket
18413 assert("console.log(ˇvar);", "console.log(varˇ);", &mut cx);
18414 assert("console.log(varˇ);", "console.log(ˇvar);", &mut cx);
18415
18416 // When outside a bracket and inside, favor jumping to the inside bracket
18417 assert(
18418 "console.log('foo', [1, 2, 3]ˇ);",
18419 "console.log('foo', ˇ[1, 2, 3]);",
18420 &mut cx,
18421 );
18422 assert(
18423 "console.log(ˇ'foo', [1, 2, 3]);",
18424 "console.log('foo'ˇ, [1, 2, 3]);",
18425 &mut cx,
18426 );
18427
18428 // Bias forward if two options are equally likely
18429 assert(
18430 "let result = curried_fun()ˇ();",
18431 "let result = curried_fun()()ˇ;",
18432 &mut cx,
18433 );
18434
18435 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
18436 assert(
18437 indoc! {"
18438 function test() {
18439 console.log('test')ˇ
18440 }"},
18441 indoc! {"
18442 function test() {
18443 console.logˇ('test')
18444 }"},
18445 &mut cx,
18446 );
18447}
18448
18449#[gpui::test]
18450async fn test_move_to_enclosing_bracket_in_markdown_code_block(cx: &mut TestAppContext) {
18451 init_test(cx, |_| {});
18452 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
18453 language_registry.add(markdown_lang());
18454 language_registry.add(rust_lang());
18455 let buffer = cx.new(|cx| {
18456 let mut buffer = language::Buffer::local(
18457 indoc! {"
18458 ```rs
18459 impl Worktree {
18460 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18461 }
18462 }
18463 ```
18464 "},
18465 cx,
18466 );
18467 buffer.set_language_registry(language_registry.clone());
18468 buffer.set_language(Some(markdown_lang()), cx);
18469 buffer
18470 });
18471 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18472 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
18473 cx.executor().run_until_parked();
18474 _ = editor.update(cx, |editor, window, cx| {
18475 // Case 1: Test outer enclosing brackets
18476 select_ranges(
18477 editor,
18478 &indoc! {"
18479 ```rs
18480 impl Worktree {
18481 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18482 }
18483 }ˇ
18484 ```
18485 "},
18486 window,
18487 cx,
18488 );
18489 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
18490 assert_text_with_selections(
18491 editor,
18492 &indoc! {"
18493 ```rs
18494 impl Worktree ˇ{
18495 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18496 }
18497 }
18498 ```
18499 "},
18500 cx,
18501 );
18502 // Case 2: Test inner enclosing brackets
18503 select_ranges(
18504 editor,
18505 &indoc! {"
18506 ```rs
18507 impl Worktree {
18508 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {
18509 }ˇ
18510 }
18511 ```
18512 "},
18513 window,
18514 cx,
18515 );
18516 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx);
18517 assert_text_with_selections(
18518 editor,
18519 &indoc! {"
18520 ```rs
18521 impl Worktree {
18522 pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{
18523 }
18524 }
18525 ```
18526 "},
18527 cx,
18528 );
18529 });
18530}
18531
18532#[gpui::test]
18533async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
18534 init_test(cx, |_| {});
18535
18536 let fs = FakeFs::new(cx.executor());
18537 fs.insert_tree(
18538 path!("/a"),
18539 json!({
18540 "main.rs": "fn main() { let a = 5; }",
18541 "other.rs": "// Test file",
18542 }),
18543 )
18544 .await;
18545 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18546
18547 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18548 language_registry.add(Arc::new(Language::new(
18549 LanguageConfig {
18550 name: "Rust".into(),
18551 matcher: LanguageMatcher {
18552 path_suffixes: vec!["rs".to_string()],
18553 ..Default::default()
18554 },
18555 brackets: BracketPairConfig {
18556 pairs: vec![BracketPair {
18557 start: "{".to_string(),
18558 end: "}".to_string(),
18559 close: true,
18560 surround: true,
18561 newline: true,
18562 }],
18563 disabled_scopes_by_bracket_ix: Vec::new(),
18564 },
18565 ..Default::default()
18566 },
18567 Some(tree_sitter_rust::LANGUAGE.into()),
18568 )));
18569 let mut fake_servers = language_registry.register_fake_lsp(
18570 "Rust",
18571 FakeLspAdapter {
18572 capabilities: lsp::ServerCapabilities {
18573 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18574 first_trigger_character: "{".to_string(),
18575 more_trigger_character: None,
18576 }),
18577 ..Default::default()
18578 },
18579 ..Default::default()
18580 },
18581 );
18582
18583 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18584
18585 let cx = &mut VisualTestContext::from_window(*workspace, cx);
18586
18587 let worktree_id = workspace
18588 .update(cx, |workspace, _, cx| {
18589 workspace.project().update(cx, |project, cx| {
18590 project.worktrees(cx).next().unwrap().read(cx).id()
18591 })
18592 })
18593 .unwrap();
18594
18595 let buffer = project
18596 .update(cx, |project, cx| {
18597 project.open_local_buffer(path!("/a/main.rs"), cx)
18598 })
18599 .await
18600 .unwrap();
18601 let editor_handle = workspace
18602 .update(cx, |workspace, window, cx| {
18603 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
18604 })
18605 .unwrap()
18606 .await
18607 .unwrap()
18608 .downcast::<Editor>()
18609 .unwrap();
18610
18611 let fake_server = fake_servers.next().await.unwrap();
18612
18613 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
18614 |params, _| async move {
18615 assert_eq!(
18616 params.text_document_position.text_document.uri,
18617 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
18618 );
18619 assert_eq!(
18620 params.text_document_position.position,
18621 lsp::Position::new(0, 21),
18622 );
18623
18624 Ok(Some(vec![lsp::TextEdit {
18625 new_text: "]".to_string(),
18626 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18627 }]))
18628 },
18629 );
18630
18631 editor_handle.update_in(cx, |editor, window, cx| {
18632 window.focus(&editor.focus_handle(cx), cx);
18633 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18634 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
18635 });
18636 editor.handle_input("{", window, cx);
18637 });
18638
18639 cx.executor().run_until_parked();
18640
18641 buffer.update(cx, |buffer, _| {
18642 assert_eq!(
18643 buffer.text(),
18644 "fn main() { let a = {5}; }",
18645 "No extra braces from on type formatting should appear in the buffer"
18646 )
18647 });
18648}
18649
18650#[gpui::test(iterations = 20, seeds(31))]
18651async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
18652 init_test(cx, |_| {});
18653
18654 let mut cx = EditorLspTestContext::new_rust(
18655 lsp::ServerCapabilities {
18656 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
18657 first_trigger_character: ".".to_string(),
18658 more_trigger_character: None,
18659 }),
18660 ..Default::default()
18661 },
18662 cx,
18663 )
18664 .await;
18665
18666 cx.update_buffer(|buffer, _| {
18667 // This causes autoindent to be async.
18668 buffer.set_sync_parse_timeout(None)
18669 });
18670
18671 cx.set_state("fn c() {\n d()ˇ\n}\n");
18672 cx.simulate_keystroke("\n");
18673 cx.run_until_parked();
18674
18675 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
18676 let mut request =
18677 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
18678 let buffer_cloned = buffer_cloned.clone();
18679 async move {
18680 buffer_cloned.update(&mut cx, |buffer, _| {
18681 assert_eq!(
18682 buffer.text(),
18683 "fn c() {\n d()\n .\n}\n",
18684 "OnTypeFormatting should triggered after autoindent applied"
18685 )
18686 });
18687
18688 Ok(Some(vec![]))
18689 }
18690 });
18691
18692 cx.simulate_keystroke(".");
18693 cx.run_until_parked();
18694
18695 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
18696 assert!(request.next().await.is_some());
18697 request.close();
18698 assert!(request.next().await.is_none());
18699}
18700
18701#[gpui::test]
18702async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
18703 init_test(cx, |_| {});
18704
18705 let fs = FakeFs::new(cx.executor());
18706 fs.insert_tree(
18707 path!("/a"),
18708 json!({
18709 "main.rs": "fn main() { let a = 5; }",
18710 "other.rs": "// Test file",
18711 }),
18712 )
18713 .await;
18714
18715 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18716
18717 let server_restarts = Arc::new(AtomicUsize::new(0));
18718 let closure_restarts = Arc::clone(&server_restarts);
18719 let language_server_name = "test language server";
18720 let language_name: LanguageName = "Rust".into();
18721
18722 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18723 language_registry.add(Arc::new(Language::new(
18724 LanguageConfig {
18725 name: language_name.clone(),
18726 matcher: LanguageMatcher {
18727 path_suffixes: vec!["rs".to_string()],
18728 ..Default::default()
18729 },
18730 ..Default::default()
18731 },
18732 Some(tree_sitter_rust::LANGUAGE.into()),
18733 )));
18734 let mut fake_servers = language_registry.register_fake_lsp(
18735 "Rust",
18736 FakeLspAdapter {
18737 name: language_server_name,
18738 initialization_options: Some(json!({
18739 "testOptionValue": true
18740 })),
18741 initializer: Some(Box::new(move |fake_server| {
18742 let task_restarts = Arc::clone(&closure_restarts);
18743 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
18744 task_restarts.fetch_add(1, atomic::Ordering::Release);
18745 futures::future::ready(Ok(()))
18746 });
18747 })),
18748 ..Default::default()
18749 },
18750 );
18751
18752 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18753 let _buffer = project
18754 .update(cx, |project, cx| {
18755 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
18756 })
18757 .await
18758 .unwrap();
18759 let _fake_server = fake_servers.next().await.unwrap();
18760 update_test_language_settings(cx, |language_settings| {
18761 language_settings.languages.0.insert(
18762 language_name.clone().0.to_string(),
18763 LanguageSettingsContent {
18764 tab_size: NonZeroU32::new(8),
18765 ..Default::default()
18766 },
18767 );
18768 });
18769 cx.executor().run_until_parked();
18770 assert_eq!(
18771 server_restarts.load(atomic::Ordering::Acquire),
18772 0,
18773 "Should not restart LSP server on an unrelated change"
18774 );
18775
18776 update_test_project_settings(cx, |project_settings| {
18777 project_settings.lsp.0.insert(
18778 "Some other server name".into(),
18779 LspSettings {
18780 binary: None,
18781 settings: None,
18782 initialization_options: Some(json!({
18783 "some other init value": false
18784 })),
18785 enable_lsp_tasks: false,
18786 fetch: None,
18787 },
18788 );
18789 });
18790 cx.executor().run_until_parked();
18791 assert_eq!(
18792 server_restarts.load(atomic::Ordering::Acquire),
18793 0,
18794 "Should not restart LSP server on an unrelated LSP settings change"
18795 );
18796
18797 update_test_project_settings(cx, |project_settings| {
18798 project_settings.lsp.0.insert(
18799 language_server_name.into(),
18800 LspSettings {
18801 binary: None,
18802 settings: None,
18803 initialization_options: Some(json!({
18804 "anotherInitValue": false
18805 })),
18806 enable_lsp_tasks: false,
18807 fetch: None,
18808 },
18809 );
18810 });
18811 cx.executor().run_until_parked();
18812 assert_eq!(
18813 server_restarts.load(atomic::Ordering::Acquire),
18814 1,
18815 "Should restart LSP server on a related LSP settings change"
18816 );
18817
18818 update_test_project_settings(cx, |project_settings| {
18819 project_settings.lsp.0.insert(
18820 language_server_name.into(),
18821 LspSettings {
18822 binary: None,
18823 settings: None,
18824 initialization_options: Some(json!({
18825 "anotherInitValue": false
18826 })),
18827 enable_lsp_tasks: false,
18828 fetch: None,
18829 },
18830 );
18831 });
18832 cx.executor().run_until_parked();
18833 assert_eq!(
18834 server_restarts.load(atomic::Ordering::Acquire),
18835 1,
18836 "Should not restart LSP server on a related LSP settings change that is the same"
18837 );
18838
18839 update_test_project_settings(cx, |project_settings| {
18840 project_settings.lsp.0.insert(
18841 language_server_name.into(),
18842 LspSettings {
18843 binary: None,
18844 settings: None,
18845 initialization_options: None,
18846 enable_lsp_tasks: false,
18847 fetch: None,
18848 },
18849 );
18850 });
18851 cx.executor().run_until_parked();
18852 assert_eq!(
18853 server_restarts.load(atomic::Ordering::Acquire),
18854 2,
18855 "Should restart LSP server on another related LSP settings change"
18856 );
18857}
18858
18859#[gpui::test]
18860async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
18861 init_test(cx, |_| {});
18862
18863 let mut cx = EditorLspTestContext::new_rust(
18864 lsp::ServerCapabilities {
18865 completion_provider: Some(lsp::CompletionOptions {
18866 trigger_characters: Some(vec![".".to_string()]),
18867 resolve_provider: Some(true),
18868 ..Default::default()
18869 }),
18870 ..Default::default()
18871 },
18872 cx,
18873 )
18874 .await;
18875
18876 cx.set_state("fn main() { let a = 2ˇ; }");
18877 cx.simulate_keystroke(".");
18878 let completion_item = lsp::CompletionItem {
18879 label: "some".into(),
18880 kind: Some(lsp::CompletionItemKind::SNIPPET),
18881 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
18882 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
18883 kind: lsp::MarkupKind::Markdown,
18884 value: "```rust\nSome(2)\n```".to_string(),
18885 })),
18886 deprecated: Some(false),
18887 sort_text: Some("fffffff2".to_string()),
18888 filter_text: Some("some".to_string()),
18889 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
18890 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18891 range: lsp::Range {
18892 start: lsp::Position {
18893 line: 0,
18894 character: 22,
18895 },
18896 end: lsp::Position {
18897 line: 0,
18898 character: 22,
18899 },
18900 },
18901 new_text: "Some(2)".to_string(),
18902 })),
18903 additional_text_edits: Some(vec![lsp::TextEdit {
18904 range: lsp::Range {
18905 start: lsp::Position {
18906 line: 0,
18907 character: 20,
18908 },
18909 end: lsp::Position {
18910 line: 0,
18911 character: 22,
18912 },
18913 },
18914 new_text: "".to_string(),
18915 }]),
18916 ..Default::default()
18917 };
18918
18919 let closure_completion_item = completion_item.clone();
18920 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18921 let task_completion_item = closure_completion_item.clone();
18922 async move {
18923 Ok(Some(lsp::CompletionResponse::Array(vec![
18924 task_completion_item,
18925 ])))
18926 }
18927 });
18928
18929 request.next().await;
18930
18931 cx.condition(|editor, _| editor.context_menu_visible())
18932 .await;
18933 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
18934 editor
18935 .confirm_completion(&ConfirmCompletion::default(), window, cx)
18936 .unwrap()
18937 });
18938 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
18939
18940 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
18941 let task_completion_item = completion_item.clone();
18942 async move { Ok(task_completion_item) }
18943 })
18944 .next()
18945 .await
18946 .unwrap();
18947 apply_additional_edits.await.unwrap();
18948 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
18949}
18950
18951#[gpui::test]
18952async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
18953 init_test(cx, |_| {});
18954
18955 let mut cx = EditorLspTestContext::new_rust(
18956 lsp::ServerCapabilities {
18957 completion_provider: Some(lsp::CompletionOptions {
18958 trigger_characters: Some(vec![".".to_string()]),
18959 resolve_provider: Some(true),
18960 ..Default::default()
18961 }),
18962 ..Default::default()
18963 },
18964 cx,
18965 )
18966 .await;
18967
18968 cx.set_state("fn main() { let a = 2ˇ; }");
18969 cx.simulate_keystroke(".");
18970
18971 let item1 = lsp::CompletionItem {
18972 label: "method id()".to_string(),
18973 filter_text: Some("id".to_string()),
18974 detail: None,
18975 documentation: None,
18976 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18977 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18978 new_text: ".id".to_string(),
18979 })),
18980 ..lsp::CompletionItem::default()
18981 };
18982
18983 let item2 = lsp::CompletionItem {
18984 label: "other".to_string(),
18985 filter_text: Some("other".to_string()),
18986 detail: None,
18987 documentation: None,
18988 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18989 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18990 new_text: ".other".to_string(),
18991 })),
18992 ..lsp::CompletionItem::default()
18993 };
18994
18995 let item1 = item1.clone();
18996 cx.set_request_handler::<lsp::request::Completion, _, _>({
18997 let item1 = item1.clone();
18998 move |_, _, _| {
18999 let item1 = item1.clone();
19000 let item2 = item2.clone();
19001 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
19002 }
19003 })
19004 .next()
19005 .await;
19006
19007 cx.condition(|editor, _| editor.context_menu_visible())
19008 .await;
19009 cx.update_editor(|editor, _, _| {
19010 let context_menu = editor.context_menu.borrow_mut();
19011 let context_menu = context_menu
19012 .as_ref()
19013 .expect("Should have the context menu deployed");
19014 match context_menu {
19015 CodeContextMenu::Completions(completions_menu) => {
19016 let completions = completions_menu.completions.borrow_mut();
19017 assert_eq!(
19018 completions
19019 .iter()
19020 .map(|completion| &completion.label.text)
19021 .collect::<Vec<_>>(),
19022 vec!["method id()", "other"]
19023 )
19024 }
19025 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
19026 }
19027 });
19028
19029 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
19030 let item1 = item1.clone();
19031 move |_, item_to_resolve, _| {
19032 let item1 = item1.clone();
19033 async move {
19034 if item1 == item_to_resolve {
19035 Ok(lsp::CompletionItem {
19036 label: "method id()".to_string(),
19037 filter_text: Some("id".to_string()),
19038 detail: Some("Now resolved!".to_string()),
19039 documentation: Some(lsp::Documentation::String("Docs".to_string())),
19040 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19041 range: lsp::Range::new(
19042 lsp::Position::new(0, 22),
19043 lsp::Position::new(0, 22),
19044 ),
19045 new_text: ".id".to_string(),
19046 })),
19047 ..lsp::CompletionItem::default()
19048 })
19049 } else {
19050 Ok(item_to_resolve)
19051 }
19052 }
19053 }
19054 })
19055 .next()
19056 .await
19057 .unwrap();
19058 cx.run_until_parked();
19059
19060 cx.update_editor(|editor, window, cx| {
19061 editor.context_menu_next(&Default::default(), window, cx);
19062 });
19063 cx.run_until_parked();
19064
19065 cx.update_editor(|editor, _, _| {
19066 let context_menu = editor.context_menu.borrow_mut();
19067 let context_menu = context_menu
19068 .as_ref()
19069 .expect("Should have the context menu deployed");
19070 match context_menu {
19071 CodeContextMenu::Completions(completions_menu) => {
19072 let completions = completions_menu.completions.borrow_mut();
19073 assert_eq!(
19074 completions
19075 .iter()
19076 .map(|completion| &completion.label.text)
19077 .collect::<Vec<_>>(),
19078 vec!["method id() Now resolved!", "other"],
19079 "Should update first completion label, but not second as the filter text did not match."
19080 );
19081 }
19082 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
19083 }
19084 });
19085}
19086
19087#[gpui::test]
19088async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
19089 init_test(cx, |_| {});
19090 let mut cx = EditorLspTestContext::new_rust(
19091 lsp::ServerCapabilities {
19092 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
19093 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
19094 completion_provider: Some(lsp::CompletionOptions {
19095 resolve_provider: Some(true),
19096 ..Default::default()
19097 }),
19098 ..Default::default()
19099 },
19100 cx,
19101 )
19102 .await;
19103 cx.set_state(indoc! {"
19104 struct TestStruct {
19105 field: i32
19106 }
19107
19108 fn mainˇ() {
19109 let unused_var = 42;
19110 let test_struct = TestStruct { field: 42 };
19111 }
19112 "});
19113 let symbol_range = cx.lsp_range(indoc! {"
19114 struct TestStruct {
19115 field: i32
19116 }
19117
19118 «fn main»() {
19119 let unused_var = 42;
19120 let test_struct = TestStruct { field: 42 };
19121 }
19122 "});
19123 let mut hover_requests =
19124 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
19125 Ok(Some(lsp::Hover {
19126 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
19127 kind: lsp::MarkupKind::Markdown,
19128 value: "Function documentation".to_string(),
19129 }),
19130 range: Some(symbol_range),
19131 }))
19132 });
19133
19134 // Case 1: Test that code action menu hide hover popover
19135 cx.dispatch_action(Hover);
19136 hover_requests.next().await;
19137 cx.condition(|editor, _| editor.hover_state.visible()).await;
19138 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
19139 move |_, _, _| async move {
19140 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
19141 lsp::CodeAction {
19142 title: "Remove unused variable".to_string(),
19143 kind: Some(CodeActionKind::QUICKFIX),
19144 edit: Some(lsp::WorkspaceEdit {
19145 changes: Some(
19146 [(
19147 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
19148 vec![lsp::TextEdit {
19149 range: lsp::Range::new(
19150 lsp::Position::new(5, 4),
19151 lsp::Position::new(5, 27),
19152 ),
19153 new_text: "".to_string(),
19154 }],
19155 )]
19156 .into_iter()
19157 .collect(),
19158 ),
19159 ..Default::default()
19160 }),
19161 ..Default::default()
19162 },
19163 )]))
19164 },
19165 );
19166 cx.update_editor(|editor, window, cx| {
19167 editor.toggle_code_actions(
19168 &ToggleCodeActions {
19169 deployed_from: None,
19170 quick_launch: false,
19171 },
19172 window,
19173 cx,
19174 );
19175 });
19176 code_action_requests.next().await;
19177 cx.run_until_parked();
19178 cx.condition(|editor, _| editor.context_menu_visible())
19179 .await;
19180 cx.update_editor(|editor, _, _| {
19181 assert!(
19182 !editor.hover_state.visible(),
19183 "Hover popover should be hidden when code action menu is shown"
19184 );
19185 // Hide code actions
19186 editor.context_menu.take();
19187 });
19188
19189 // Case 2: Test that code completions hide hover popover
19190 cx.dispatch_action(Hover);
19191 hover_requests.next().await;
19192 cx.condition(|editor, _| editor.hover_state.visible()).await;
19193 let counter = Arc::new(AtomicUsize::new(0));
19194 let mut completion_requests =
19195 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19196 let counter = counter.clone();
19197 async move {
19198 counter.fetch_add(1, atomic::Ordering::Release);
19199 Ok(Some(lsp::CompletionResponse::Array(vec![
19200 lsp::CompletionItem {
19201 label: "main".into(),
19202 kind: Some(lsp::CompletionItemKind::FUNCTION),
19203 detail: Some("() -> ()".to_string()),
19204 ..Default::default()
19205 },
19206 lsp::CompletionItem {
19207 label: "TestStruct".into(),
19208 kind: Some(lsp::CompletionItemKind::STRUCT),
19209 detail: Some("struct TestStruct".to_string()),
19210 ..Default::default()
19211 },
19212 ])))
19213 }
19214 });
19215 cx.update_editor(|editor, window, cx| {
19216 editor.show_completions(&ShowCompletions, window, cx);
19217 });
19218 completion_requests.next().await;
19219 cx.condition(|editor, _| editor.context_menu_visible())
19220 .await;
19221 cx.update_editor(|editor, _, _| {
19222 assert!(
19223 !editor.hover_state.visible(),
19224 "Hover popover should be hidden when completion menu is shown"
19225 );
19226 });
19227}
19228
19229#[gpui::test]
19230async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
19231 init_test(cx, |_| {});
19232
19233 let mut cx = EditorLspTestContext::new_rust(
19234 lsp::ServerCapabilities {
19235 completion_provider: Some(lsp::CompletionOptions {
19236 trigger_characters: Some(vec![".".to_string()]),
19237 resolve_provider: Some(true),
19238 ..Default::default()
19239 }),
19240 ..Default::default()
19241 },
19242 cx,
19243 )
19244 .await;
19245
19246 cx.set_state("fn main() { let a = 2ˇ; }");
19247 cx.simulate_keystroke(".");
19248
19249 let unresolved_item_1 = lsp::CompletionItem {
19250 label: "id".to_string(),
19251 filter_text: Some("id".to_string()),
19252 detail: None,
19253 documentation: None,
19254 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19255 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
19256 new_text: ".id".to_string(),
19257 })),
19258 ..lsp::CompletionItem::default()
19259 };
19260 let resolved_item_1 = lsp::CompletionItem {
19261 additional_text_edits: Some(vec![lsp::TextEdit {
19262 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
19263 new_text: "!!".to_string(),
19264 }]),
19265 ..unresolved_item_1.clone()
19266 };
19267 let unresolved_item_2 = lsp::CompletionItem {
19268 label: "other".to_string(),
19269 filter_text: Some("other".to_string()),
19270 detail: None,
19271 documentation: None,
19272 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19273 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
19274 new_text: ".other".to_string(),
19275 })),
19276 ..lsp::CompletionItem::default()
19277 };
19278 let resolved_item_2 = lsp::CompletionItem {
19279 additional_text_edits: Some(vec![lsp::TextEdit {
19280 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
19281 new_text: "??".to_string(),
19282 }]),
19283 ..unresolved_item_2.clone()
19284 };
19285
19286 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
19287 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
19288 cx.lsp
19289 .server
19290 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
19291 let unresolved_item_1 = unresolved_item_1.clone();
19292 let resolved_item_1 = resolved_item_1.clone();
19293 let unresolved_item_2 = unresolved_item_2.clone();
19294 let resolved_item_2 = resolved_item_2.clone();
19295 let resolve_requests_1 = resolve_requests_1.clone();
19296 let resolve_requests_2 = resolve_requests_2.clone();
19297 move |unresolved_request, _| {
19298 let unresolved_item_1 = unresolved_item_1.clone();
19299 let resolved_item_1 = resolved_item_1.clone();
19300 let unresolved_item_2 = unresolved_item_2.clone();
19301 let resolved_item_2 = resolved_item_2.clone();
19302 let resolve_requests_1 = resolve_requests_1.clone();
19303 let resolve_requests_2 = resolve_requests_2.clone();
19304 async move {
19305 if unresolved_request == unresolved_item_1 {
19306 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
19307 Ok(resolved_item_1.clone())
19308 } else if unresolved_request == unresolved_item_2 {
19309 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
19310 Ok(resolved_item_2.clone())
19311 } else {
19312 panic!("Unexpected completion item {unresolved_request:?}")
19313 }
19314 }
19315 }
19316 })
19317 .detach();
19318
19319 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19320 let unresolved_item_1 = unresolved_item_1.clone();
19321 let unresolved_item_2 = unresolved_item_2.clone();
19322 async move {
19323 Ok(Some(lsp::CompletionResponse::Array(vec![
19324 unresolved_item_1,
19325 unresolved_item_2,
19326 ])))
19327 }
19328 })
19329 .next()
19330 .await;
19331
19332 cx.condition(|editor, _| editor.context_menu_visible())
19333 .await;
19334 cx.update_editor(|editor, _, _| {
19335 let context_menu = editor.context_menu.borrow_mut();
19336 let context_menu = context_menu
19337 .as_ref()
19338 .expect("Should have the context menu deployed");
19339 match context_menu {
19340 CodeContextMenu::Completions(completions_menu) => {
19341 let completions = completions_menu.completions.borrow_mut();
19342 assert_eq!(
19343 completions
19344 .iter()
19345 .map(|completion| &completion.label.text)
19346 .collect::<Vec<_>>(),
19347 vec!["id", "other"]
19348 )
19349 }
19350 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
19351 }
19352 });
19353 cx.run_until_parked();
19354
19355 cx.update_editor(|editor, window, cx| {
19356 editor.context_menu_next(&ContextMenuNext, window, cx);
19357 });
19358 cx.run_until_parked();
19359 cx.update_editor(|editor, window, cx| {
19360 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
19361 });
19362 cx.run_until_parked();
19363 cx.update_editor(|editor, window, cx| {
19364 editor.context_menu_next(&ContextMenuNext, window, cx);
19365 });
19366 cx.run_until_parked();
19367 cx.update_editor(|editor, window, cx| {
19368 editor
19369 .compose_completion(&ComposeCompletion::default(), window, cx)
19370 .expect("No task returned")
19371 })
19372 .await
19373 .expect("Completion failed");
19374 cx.run_until_parked();
19375
19376 cx.update_editor(|editor, _, cx| {
19377 assert_eq!(
19378 resolve_requests_1.load(atomic::Ordering::Acquire),
19379 1,
19380 "Should always resolve once despite multiple selections"
19381 );
19382 assert_eq!(
19383 resolve_requests_2.load(atomic::Ordering::Acquire),
19384 1,
19385 "Should always resolve once after multiple selections and applying the completion"
19386 );
19387 assert_eq!(
19388 editor.text(cx),
19389 "fn main() { let a = ??.other; }",
19390 "Should use resolved data when applying the completion"
19391 );
19392 });
19393}
19394
19395#[gpui::test]
19396async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
19397 init_test(cx, |_| {});
19398
19399 let item_0 = lsp::CompletionItem {
19400 label: "abs".into(),
19401 insert_text: Some("abs".into()),
19402 data: Some(json!({ "very": "special"})),
19403 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
19404 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
19405 lsp::InsertReplaceEdit {
19406 new_text: "abs".to_string(),
19407 insert: lsp::Range::default(),
19408 replace: lsp::Range::default(),
19409 },
19410 )),
19411 ..lsp::CompletionItem::default()
19412 };
19413 let items = iter::once(item_0.clone())
19414 .chain((11..51).map(|i| lsp::CompletionItem {
19415 label: format!("item_{}", i),
19416 insert_text: Some(format!("item_{}", i)),
19417 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
19418 ..lsp::CompletionItem::default()
19419 }))
19420 .collect::<Vec<_>>();
19421
19422 let default_commit_characters = vec!["?".to_string()];
19423 let default_data = json!({ "default": "data"});
19424 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
19425 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
19426 let default_edit_range = lsp::Range {
19427 start: lsp::Position {
19428 line: 0,
19429 character: 5,
19430 },
19431 end: lsp::Position {
19432 line: 0,
19433 character: 5,
19434 },
19435 };
19436
19437 let mut cx = EditorLspTestContext::new_rust(
19438 lsp::ServerCapabilities {
19439 completion_provider: Some(lsp::CompletionOptions {
19440 trigger_characters: Some(vec![".".to_string()]),
19441 resolve_provider: Some(true),
19442 ..Default::default()
19443 }),
19444 ..Default::default()
19445 },
19446 cx,
19447 )
19448 .await;
19449
19450 cx.set_state("fn main() { let a = 2ˇ; }");
19451 cx.simulate_keystroke(".");
19452
19453 let completion_data = default_data.clone();
19454 let completion_characters = default_commit_characters.clone();
19455 let completion_items = items.clone();
19456 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
19457 let default_data = completion_data.clone();
19458 let default_commit_characters = completion_characters.clone();
19459 let items = completion_items.clone();
19460 async move {
19461 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
19462 items,
19463 item_defaults: Some(lsp::CompletionListItemDefaults {
19464 data: Some(default_data.clone()),
19465 commit_characters: Some(default_commit_characters.clone()),
19466 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
19467 default_edit_range,
19468 )),
19469 insert_text_format: Some(default_insert_text_format),
19470 insert_text_mode: Some(default_insert_text_mode),
19471 }),
19472 ..lsp::CompletionList::default()
19473 })))
19474 }
19475 })
19476 .next()
19477 .await;
19478
19479 let resolved_items = Arc::new(Mutex::new(Vec::new()));
19480 cx.lsp
19481 .server
19482 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
19483 let closure_resolved_items = resolved_items.clone();
19484 move |item_to_resolve, _| {
19485 let closure_resolved_items = closure_resolved_items.clone();
19486 async move {
19487 closure_resolved_items.lock().push(item_to_resolve.clone());
19488 Ok(item_to_resolve)
19489 }
19490 }
19491 })
19492 .detach();
19493
19494 cx.condition(|editor, _| editor.context_menu_visible())
19495 .await;
19496 cx.run_until_parked();
19497 cx.update_editor(|editor, _, _| {
19498 let menu = editor.context_menu.borrow_mut();
19499 match menu.as_ref().expect("should have the completions menu") {
19500 CodeContextMenu::Completions(completions_menu) => {
19501 assert_eq!(
19502 completions_menu
19503 .entries
19504 .borrow()
19505 .iter()
19506 .map(|mat| mat.string.clone())
19507 .collect::<Vec<String>>(),
19508 items
19509 .iter()
19510 .map(|completion| completion.label.clone())
19511 .collect::<Vec<String>>()
19512 );
19513 }
19514 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
19515 }
19516 });
19517 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
19518 // with 4 from the end.
19519 assert_eq!(
19520 *resolved_items.lock(),
19521 [&items[0..16], &items[items.len() - 4..items.len()]]
19522 .concat()
19523 .iter()
19524 .cloned()
19525 .map(|mut item| {
19526 if item.data.is_none() {
19527 item.data = Some(default_data.clone());
19528 }
19529 item
19530 })
19531 .collect::<Vec<lsp::CompletionItem>>(),
19532 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
19533 );
19534 resolved_items.lock().clear();
19535
19536 cx.update_editor(|editor, window, cx| {
19537 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
19538 });
19539 cx.run_until_parked();
19540 // Completions that have already been resolved are skipped.
19541 assert_eq!(
19542 *resolved_items.lock(),
19543 items[items.len() - 17..items.len() - 4]
19544 .iter()
19545 .cloned()
19546 .map(|mut item| {
19547 if item.data.is_none() {
19548 item.data = Some(default_data.clone());
19549 }
19550 item
19551 })
19552 .collect::<Vec<lsp::CompletionItem>>()
19553 );
19554 resolved_items.lock().clear();
19555}
19556
19557#[gpui::test]
19558async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
19559 init_test(cx, |_| {});
19560
19561 let mut cx = EditorLspTestContext::new(
19562 Language::new(
19563 LanguageConfig {
19564 matcher: LanguageMatcher {
19565 path_suffixes: vec!["jsx".into()],
19566 ..Default::default()
19567 },
19568 overrides: [(
19569 "element".into(),
19570 LanguageConfigOverride {
19571 completion_query_characters: Override::Set(['-'].into_iter().collect()),
19572 ..Default::default()
19573 },
19574 )]
19575 .into_iter()
19576 .collect(),
19577 ..Default::default()
19578 },
19579 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
19580 )
19581 .with_override_query("(jsx_self_closing_element) @element")
19582 .unwrap(),
19583 lsp::ServerCapabilities {
19584 completion_provider: Some(lsp::CompletionOptions {
19585 trigger_characters: Some(vec![":".to_string()]),
19586 ..Default::default()
19587 }),
19588 ..Default::default()
19589 },
19590 cx,
19591 )
19592 .await;
19593
19594 cx.lsp
19595 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
19596 Ok(Some(lsp::CompletionResponse::Array(vec![
19597 lsp::CompletionItem {
19598 label: "bg-blue".into(),
19599 ..Default::default()
19600 },
19601 lsp::CompletionItem {
19602 label: "bg-red".into(),
19603 ..Default::default()
19604 },
19605 lsp::CompletionItem {
19606 label: "bg-yellow".into(),
19607 ..Default::default()
19608 },
19609 ])))
19610 });
19611
19612 cx.set_state(r#"<p class="bgˇ" />"#);
19613
19614 // Trigger completion when typing a dash, because the dash is an extra
19615 // word character in the 'element' scope, which contains the cursor.
19616 cx.simulate_keystroke("-");
19617 cx.executor().run_until_parked();
19618 cx.update_editor(|editor, _, _| {
19619 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19620 {
19621 assert_eq!(
19622 completion_menu_entries(menu),
19623 &["bg-blue", "bg-red", "bg-yellow"]
19624 );
19625 } else {
19626 panic!("expected completion menu to be open");
19627 }
19628 });
19629
19630 cx.simulate_keystroke("l");
19631 cx.executor().run_until_parked();
19632 cx.update_editor(|editor, _, _| {
19633 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19634 {
19635 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
19636 } else {
19637 panic!("expected completion menu to be open");
19638 }
19639 });
19640
19641 // When filtering completions, consider the character after the '-' to
19642 // be the start of a subword.
19643 cx.set_state(r#"<p class="yelˇ" />"#);
19644 cx.simulate_keystroke("l");
19645 cx.executor().run_until_parked();
19646 cx.update_editor(|editor, _, _| {
19647 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
19648 {
19649 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
19650 } else {
19651 panic!("expected completion menu to be open");
19652 }
19653 });
19654}
19655
19656fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
19657 let entries = menu.entries.borrow();
19658 entries.iter().map(|mat| mat.string.clone()).collect()
19659}
19660
19661#[gpui::test]
19662async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
19663 init_test(cx, |settings| {
19664 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19665 });
19666
19667 let fs = FakeFs::new(cx.executor());
19668 fs.insert_file(path!("/file.ts"), Default::default()).await;
19669
19670 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
19671 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19672
19673 language_registry.add(Arc::new(Language::new(
19674 LanguageConfig {
19675 name: "TypeScript".into(),
19676 matcher: LanguageMatcher {
19677 path_suffixes: vec!["ts".to_string()],
19678 ..Default::default()
19679 },
19680 ..Default::default()
19681 },
19682 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19683 )));
19684 update_test_language_settings(cx, |settings| {
19685 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19686 });
19687
19688 let test_plugin = "test_plugin";
19689 let _ = language_registry.register_fake_lsp(
19690 "TypeScript",
19691 FakeLspAdapter {
19692 prettier_plugins: vec![test_plugin],
19693 ..Default::default()
19694 },
19695 );
19696
19697 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19698 let buffer = project
19699 .update(cx, |project, cx| {
19700 project.open_local_buffer(path!("/file.ts"), cx)
19701 })
19702 .await
19703 .unwrap();
19704
19705 let buffer_text = "one\ntwo\nthree\n";
19706 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19707 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19708 editor.update_in(cx, |editor, window, cx| {
19709 editor.set_text(buffer_text, window, cx)
19710 });
19711
19712 editor
19713 .update_in(cx, |editor, window, cx| {
19714 editor.perform_format(
19715 project.clone(),
19716 FormatTrigger::Manual,
19717 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19718 window,
19719 cx,
19720 )
19721 })
19722 .unwrap()
19723 .await;
19724 assert_eq!(
19725 editor.update(cx, |editor, cx| editor.text(cx)),
19726 buffer_text.to_string() + prettier_format_suffix,
19727 "Test prettier formatting was not applied to the original buffer text",
19728 );
19729
19730 update_test_language_settings(cx, |settings| {
19731 settings.defaults.formatter = Some(FormatterList::default())
19732 });
19733 let format = editor.update_in(cx, |editor, window, cx| {
19734 editor.perform_format(
19735 project.clone(),
19736 FormatTrigger::Manual,
19737 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19738 window,
19739 cx,
19740 )
19741 });
19742 format.await.unwrap();
19743 assert_eq!(
19744 editor.update(cx, |editor, cx| editor.text(cx)),
19745 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
19746 "Autoformatting (via test prettier) was not applied to the original buffer text",
19747 );
19748}
19749
19750#[gpui::test]
19751async fn test_document_format_with_prettier_explicit_language(cx: &mut TestAppContext) {
19752 init_test(cx, |settings| {
19753 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
19754 });
19755
19756 let fs = FakeFs::new(cx.executor());
19757 fs.insert_file(path!("/file.settings"), Default::default())
19758 .await;
19759
19760 let project = Project::test(fs, [path!("/file.settings").as_ref()], cx).await;
19761 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19762
19763 let ts_lang = Arc::new(Language::new(
19764 LanguageConfig {
19765 name: "TypeScript".into(),
19766 matcher: LanguageMatcher {
19767 path_suffixes: vec!["ts".to_string()],
19768 ..LanguageMatcher::default()
19769 },
19770 prettier_parser_name: Some("typescript".to_string()),
19771 ..LanguageConfig::default()
19772 },
19773 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19774 ));
19775
19776 language_registry.add(ts_lang.clone());
19777
19778 update_test_language_settings(cx, |settings| {
19779 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
19780 });
19781
19782 let test_plugin = "test_plugin";
19783 let _ = language_registry.register_fake_lsp(
19784 "TypeScript",
19785 FakeLspAdapter {
19786 prettier_plugins: vec![test_plugin],
19787 ..Default::default()
19788 },
19789 );
19790
19791 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
19792 let buffer = project
19793 .update(cx, |project, cx| {
19794 project.open_local_buffer(path!("/file.settings"), cx)
19795 })
19796 .await
19797 .unwrap();
19798
19799 project.update(cx, |project, cx| {
19800 project.set_language_for_buffer(&buffer, ts_lang, cx)
19801 });
19802
19803 let buffer_text = "one\ntwo\nthree\n";
19804 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
19805 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
19806 editor.update_in(cx, |editor, window, cx| {
19807 editor.set_text(buffer_text, window, cx)
19808 });
19809
19810 editor
19811 .update_in(cx, |editor, window, cx| {
19812 editor.perform_format(
19813 project.clone(),
19814 FormatTrigger::Manual,
19815 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19816 window,
19817 cx,
19818 )
19819 })
19820 .unwrap()
19821 .await;
19822 assert_eq!(
19823 editor.update(cx, |editor, cx| editor.text(cx)),
19824 buffer_text.to_string() + prettier_format_suffix + "\ntypescript",
19825 "Test prettier formatting was not applied to the original buffer text",
19826 );
19827
19828 update_test_language_settings(cx, |settings| {
19829 settings.defaults.formatter = Some(FormatterList::default())
19830 });
19831 let format = editor.update_in(cx, |editor, window, cx| {
19832 editor.perform_format(
19833 project.clone(),
19834 FormatTrigger::Manual,
19835 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
19836 window,
19837 cx,
19838 )
19839 });
19840 format.await.unwrap();
19841
19842 assert_eq!(
19843 editor.update(cx, |editor, cx| editor.text(cx)),
19844 buffer_text.to_string()
19845 + prettier_format_suffix
19846 + "\ntypescript\n"
19847 + prettier_format_suffix
19848 + "\ntypescript",
19849 "Autoformatting (via test prettier) was not applied to the original buffer text",
19850 );
19851}
19852
19853#[gpui::test]
19854async fn test_addition_reverts(cx: &mut TestAppContext) {
19855 init_test(cx, |_| {});
19856 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19857 let base_text = indoc! {r#"
19858 struct Row;
19859 struct Row1;
19860 struct Row2;
19861
19862 struct Row4;
19863 struct Row5;
19864 struct Row6;
19865
19866 struct Row8;
19867 struct Row9;
19868 struct Row10;"#};
19869
19870 // When addition hunks are not adjacent to carets, no hunk revert is performed
19871 assert_hunk_revert(
19872 indoc! {r#"struct Row;
19873 struct Row1;
19874 struct Row1.1;
19875 struct Row1.2;
19876 struct Row2;ˇ
19877
19878 struct Row4;
19879 struct Row5;
19880 struct Row6;
19881
19882 struct Row8;
19883 ˇstruct Row9;
19884 struct Row9.1;
19885 struct Row9.2;
19886 struct Row9.3;
19887 struct Row10;"#},
19888 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19889 indoc! {r#"struct Row;
19890 struct Row1;
19891 struct Row1.1;
19892 struct Row1.2;
19893 struct Row2;ˇ
19894
19895 struct Row4;
19896 struct Row5;
19897 struct Row6;
19898
19899 struct Row8;
19900 ˇstruct Row9;
19901 struct Row9.1;
19902 struct Row9.2;
19903 struct Row9.3;
19904 struct Row10;"#},
19905 base_text,
19906 &mut cx,
19907 );
19908 // Same for selections
19909 assert_hunk_revert(
19910 indoc! {r#"struct Row;
19911 struct Row1;
19912 struct Row2;
19913 struct Row2.1;
19914 struct Row2.2;
19915 «ˇ
19916 struct Row4;
19917 struct» Row5;
19918 «struct Row6;
19919 ˇ»
19920 struct Row9.1;
19921 struct Row9.2;
19922 struct Row9.3;
19923 struct Row8;
19924 struct Row9;
19925 struct Row10;"#},
19926 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
19927 indoc! {r#"struct Row;
19928 struct Row1;
19929 struct Row2;
19930 struct Row2.1;
19931 struct Row2.2;
19932 «ˇ
19933 struct Row4;
19934 struct» Row5;
19935 «struct Row6;
19936 ˇ»
19937 struct Row9.1;
19938 struct Row9.2;
19939 struct Row9.3;
19940 struct Row8;
19941 struct Row9;
19942 struct Row10;"#},
19943 base_text,
19944 &mut cx,
19945 );
19946
19947 // When carets and selections intersect the addition hunks, those are reverted.
19948 // Adjacent carets got merged.
19949 assert_hunk_revert(
19950 indoc! {r#"struct Row;
19951 ˇ// something on the top
19952 struct Row1;
19953 struct Row2;
19954 struct Roˇw3.1;
19955 struct Row2.2;
19956 struct Row2.3;ˇ
19957
19958 struct Row4;
19959 struct ˇRow5.1;
19960 struct Row5.2;
19961 struct «Rowˇ»5.3;
19962 struct Row5;
19963 struct Row6;
19964 ˇ
19965 struct Row9.1;
19966 struct «Rowˇ»9.2;
19967 struct «ˇRow»9.3;
19968 struct Row8;
19969 struct Row9;
19970 «ˇ// something on bottom»
19971 struct Row10;"#},
19972 vec![
19973 DiffHunkStatusKind::Added,
19974 DiffHunkStatusKind::Added,
19975 DiffHunkStatusKind::Added,
19976 DiffHunkStatusKind::Added,
19977 DiffHunkStatusKind::Added,
19978 ],
19979 indoc! {r#"struct Row;
19980 ˇstruct Row1;
19981 struct Row2;
19982 ˇ
19983 struct Row4;
19984 ˇstruct Row5;
19985 struct Row6;
19986 ˇ
19987 ˇstruct Row8;
19988 struct Row9;
19989 ˇstruct Row10;"#},
19990 base_text,
19991 &mut cx,
19992 );
19993}
19994
19995#[gpui::test]
19996async fn test_modification_reverts(cx: &mut TestAppContext) {
19997 init_test(cx, |_| {});
19998 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
19999 let base_text = indoc! {r#"
20000 struct Row;
20001 struct Row1;
20002 struct Row2;
20003
20004 struct Row4;
20005 struct Row5;
20006 struct Row6;
20007
20008 struct Row8;
20009 struct Row9;
20010 struct Row10;"#};
20011
20012 // Modification hunks behave the same as the addition ones.
20013 assert_hunk_revert(
20014 indoc! {r#"struct Row;
20015 struct Row1;
20016 struct Row33;
20017 ˇ
20018 struct Row4;
20019 struct Row5;
20020 struct Row6;
20021 ˇ
20022 struct Row99;
20023 struct Row9;
20024 struct Row10;"#},
20025 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
20026 indoc! {r#"struct Row;
20027 struct Row1;
20028 struct Row33;
20029 ˇ
20030 struct Row4;
20031 struct Row5;
20032 struct Row6;
20033 ˇ
20034 struct Row99;
20035 struct Row9;
20036 struct Row10;"#},
20037 base_text,
20038 &mut cx,
20039 );
20040 assert_hunk_revert(
20041 indoc! {r#"struct Row;
20042 struct Row1;
20043 struct Row33;
20044 «ˇ
20045 struct Row4;
20046 struct» Row5;
20047 «struct Row6;
20048 ˇ»
20049 struct Row99;
20050 struct Row9;
20051 struct Row10;"#},
20052 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
20053 indoc! {r#"struct Row;
20054 struct Row1;
20055 struct Row33;
20056 «ˇ
20057 struct Row4;
20058 struct» Row5;
20059 «struct Row6;
20060 ˇ»
20061 struct Row99;
20062 struct Row9;
20063 struct Row10;"#},
20064 base_text,
20065 &mut cx,
20066 );
20067
20068 assert_hunk_revert(
20069 indoc! {r#"ˇstruct Row1.1;
20070 struct Row1;
20071 «ˇstr»uct Row22;
20072
20073 struct ˇRow44;
20074 struct Row5;
20075 struct «Rˇ»ow66;ˇ
20076
20077 «struˇ»ct Row88;
20078 struct Row9;
20079 struct Row1011;ˇ"#},
20080 vec![
20081 DiffHunkStatusKind::Modified,
20082 DiffHunkStatusKind::Modified,
20083 DiffHunkStatusKind::Modified,
20084 DiffHunkStatusKind::Modified,
20085 DiffHunkStatusKind::Modified,
20086 DiffHunkStatusKind::Modified,
20087 ],
20088 indoc! {r#"struct Row;
20089 ˇstruct Row1;
20090 struct Row2;
20091 ˇ
20092 struct Row4;
20093 ˇstruct Row5;
20094 struct Row6;
20095 ˇ
20096 struct Row8;
20097 ˇstruct Row9;
20098 struct Row10;ˇ"#},
20099 base_text,
20100 &mut cx,
20101 );
20102}
20103
20104#[gpui::test]
20105async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
20106 init_test(cx, |_| {});
20107 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
20108 let base_text = indoc! {r#"
20109 one
20110
20111 two
20112 three
20113 "#};
20114
20115 cx.set_head_text(base_text);
20116 cx.set_state("\nˇ\n");
20117 cx.executor().run_until_parked();
20118 cx.update_editor(|editor, _window, cx| {
20119 editor.expand_selected_diff_hunks(cx);
20120 });
20121 cx.executor().run_until_parked();
20122 cx.update_editor(|editor, window, cx| {
20123 editor.backspace(&Default::default(), window, cx);
20124 });
20125 cx.run_until_parked();
20126 cx.assert_state_with_diff(
20127 indoc! {r#"
20128
20129 - two
20130 - threeˇ
20131 +
20132 "#}
20133 .to_string(),
20134 );
20135}
20136
20137#[gpui::test]
20138async fn test_deletion_reverts(cx: &mut TestAppContext) {
20139 init_test(cx, |_| {});
20140 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
20141 let base_text = indoc! {r#"struct Row;
20142struct Row1;
20143struct Row2;
20144
20145struct Row4;
20146struct Row5;
20147struct Row6;
20148
20149struct Row8;
20150struct Row9;
20151struct Row10;"#};
20152
20153 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
20154 assert_hunk_revert(
20155 indoc! {r#"struct Row;
20156 struct Row2;
20157
20158 ˇstruct Row4;
20159 struct Row5;
20160 struct Row6;
20161 ˇ
20162 struct Row8;
20163 struct Row10;"#},
20164 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
20165 indoc! {r#"struct Row;
20166 struct Row2;
20167
20168 ˇstruct Row4;
20169 struct Row5;
20170 struct Row6;
20171 ˇ
20172 struct Row8;
20173 struct Row10;"#},
20174 base_text,
20175 &mut cx,
20176 );
20177 assert_hunk_revert(
20178 indoc! {r#"struct Row;
20179 struct Row2;
20180
20181 «ˇstruct Row4;
20182 struct» Row5;
20183 «struct Row6;
20184 ˇ»
20185 struct Row8;
20186 struct Row10;"#},
20187 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
20188 indoc! {r#"struct Row;
20189 struct Row2;
20190
20191 «ˇstruct Row4;
20192 struct» Row5;
20193 «struct Row6;
20194 ˇ»
20195 struct Row8;
20196 struct Row10;"#},
20197 base_text,
20198 &mut cx,
20199 );
20200
20201 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
20202 assert_hunk_revert(
20203 indoc! {r#"struct Row;
20204 ˇstruct Row2;
20205
20206 struct Row4;
20207 struct Row5;
20208 struct Row6;
20209
20210 struct Row8;ˇ
20211 struct Row10;"#},
20212 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
20213 indoc! {r#"struct Row;
20214 struct Row1;
20215 ˇstruct Row2;
20216
20217 struct Row4;
20218 struct Row5;
20219 struct Row6;
20220
20221 struct Row8;ˇ
20222 struct Row9;
20223 struct Row10;"#},
20224 base_text,
20225 &mut cx,
20226 );
20227 assert_hunk_revert(
20228 indoc! {r#"struct Row;
20229 struct Row2«ˇ;
20230 struct Row4;
20231 struct» Row5;
20232 «struct Row6;
20233
20234 struct Row8;ˇ»
20235 struct Row10;"#},
20236 vec![
20237 DiffHunkStatusKind::Deleted,
20238 DiffHunkStatusKind::Deleted,
20239 DiffHunkStatusKind::Deleted,
20240 ],
20241 indoc! {r#"struct Row;
20242 struct Row1;
20243 struct Row2«ˇ;
20244
20245 struct Row4;
20246 struct» Row5;
20247 «struct Row6;
20248
20249 struct Row8;ˇ»
20250 struct Row9;
20251 struct Row10;"#},
20252 base_text,
20253 &mut cx,
20254 );
20255}
20256
20257#[gpui::test]
20258async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
20259 init_test(cx, |_| {});
20260
20261 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
20262 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
20263 let base_text_3 =
20264 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
20265
20266 let text_1 = edit_first_char_of_every_line(base_text_1);
20267 let text_2 = edit_first_char_of_every_line(base_text_2);
20268 let text_3 = edit_first_char_of_every_line(base_text_3);
20269
20270 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
20271 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
20272 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
20273
20274 let multibuffer = cx.new(|cx| {
20275 let mut multibuffer = MultiBuffer::new(ReadWrite);
20276 multibuffer.push_excerpts(
20277 buffer_1.clone(),
20278 [
20279 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20280 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20281 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20282 ],
20283 cx,
20284 );
20285 multibuffer.push_excerpts(
20286 buffer_2.clone(),
20287 [
20288 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20289 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20290 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20291 ],
20292 cx,
20293 );
20294 multibuffer.push_excerpts(
20295 buffer_3.clone(),
20296 [
20297 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20298 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20299 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20300 ],
20301 cx,
20302 );
20303 multibuffer
20304 });
20305
20306 let fs = FakeFs::new(cx.executor());
20307 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20308 let (editor, cx) = cx
20309 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
20310 editor.update_in(cx, |editor, _window, cx| {
20311 for (buffer, diff_base) in [
20312 (buffer_1.clone(), base_text_1),
20313 (buffer_2.clone(), base_text_2),
20314 (buffer_3.clone(), base_text_3),
20315 ] {
20316 let diff = cx.new(|cx| {
20317 BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
20318 });
20319 editor
20320 .buffer
20321 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
20322 }
20323 });
20324 cx.executor().run_until_parked();
20325
20326 editor.update_in(cx, |editor, window, cx| {
20327 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}");
20328 editor.select_all(&SelectAll, window, cx);
20329 editor.git_restore(&Default::default(), window, cx);
20330 });
20331 cx.executor().run_until_parked();
20332
20333 // When all ranges are selected, all buffer hunks are reverted.
20334 editor.update(cx, |editor, cx| {
20335 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");
20336 });
20337 buffer_1.update(cx, |buffer, _| {
20338 assert_eq!(buffer.text(), base_text_1);
20339 });
20340 buffer_2.update(cx, |buffer, _| {
20341 assert_eq!(buffer.text(), base_text_2);
20342 });
20343 buffer_3.update(cx, |buffer, _| {
20344 assert_eq!(buffer.text(), base_text_3);
20345 });
20346
20347 editor.update_in(cx, |editor, window, cx| {
20348 editor.undo(&Default::default(), window, cx);
20349 });
20350
20351 editor.update_in(cx, |editor, window, cx| {
20352 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20353 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
20354 });
20355 editor.git_restore(&Default::default(), window, cx);
20356 });
20357
20358 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
20359 // but not affect buffer_2 and its related excerpts.
20360 editor.update(cx, |editor, cx| {
20361 assert_eq!(
20362 editor.text(cx),
20363 "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}"
20364 );
20365 });
20366 buffer_1.update(cx, |buffer, _| {
20367 assert_eq!(buffer.text(), base_text_1);
20368 });
20369 buffer_2.update(cx, |buffer, _| {
20370 assert_eq!(
20371 buffer.text(),
20372 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
20373 );
20374 });
20375 buffer_3.update(cx, |buffer, _| {
20376 assert_eq!(
20377 buffer.text(),
20378 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
20379 );
20380 });
20381
20382 fn edit_first_char_of_every_line(text: &str) -> String {
20383 text.split('\n')
20384 .map(|line| format!("X{}", &line[1..]))
20385 .collect::<Vec<_>>()
20386 .join("\n")
20387 }
20388}
20389
20390#[gpui::test]
20391async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
20392 init_test(cx, |_| {});
20393
20394 let cols = 4;
20395 let rows = 10;
20396 let sample_text_1 = sample_text(rows, cols, 'a');
20397 assert_eq!(
20398 sample_text_1,
20399 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
20400 );
20401 let sample_text_2 = sample_text(rows, cols, 'l');
20402 assert_eq!(
20403 sample_text_2,
20404 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
20405 );
20406 let sample_text_3 = sample_text(rows, cols, 'v');
20407 assert_eq!(
20408 sample_text_3,
20409 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
20410 );
20411
20412 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
20413 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
20414 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
20415
20416 let multi_buffer = cx.new(|cx| {
20417 let mut multibuffer = MultiBuffer::new(ReadWrite);
20418 multibuffer.push_excerpts(
20419 buffer_1.clone(),
20420 [
20421 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20422 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20423 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20424 ],
20425 cx,
20426 );
20427 multibuffer.push_excerpts(
20428 buffer_2.clone(),
20429 [
20430 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20431 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20432 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20433 ],
20434 cx,
20435 );
20436 multibuffer.push_excerpts(
20437 buffer_3.clone(),
20438 [
20439 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20440 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20441 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
20442 ],
20443 cx,
20444 );
20445 multibuffer
20446 });
20447
20448 let fs = FakeFs::new(cx.executor());
20449 fs.insert_tree(
20450 "/a",
20451 json!({
20452 "main.rs": sample_text_1,
20453 "other.rs": sample_text_2,
20454 "lib.rs": sample_text_3,
20455 }),
20456 )
20457 .await;
20458 let project = Project::test(fs, ["/a".as_ref()], cx).await;
20459 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20460 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20461 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
20462 Editor::new(
20463 EditorMode::full(),
20464 multi_buffer,
20465 Some(project.clone()),
20466 window,
20467 cx,
20468 )
20469 });
20470 let multibuffer_item_id = workspace
20471 .update(cx, |workspace, window, cx| {
20472 assert!(
20473 workspace.active_item(cx).is_none(),
20474 "active item should be None before the first item is added"
20475 );
20476 workspace.add_item_to_active_pane(
20477 Box::new(multi_buffer_editor.clone()),
20478 None,
20479 true,
20480 window,
20481 cx,
20482 );
20483 let active_item = workspace
20484 .active_item(cx)
20485 .expect("should have an active item after adding the multi buffer");
20486 assert_eq!(
20487 active_item.buffer_kind(cx),
20488 ItemBufferKind::Multibuffer,
20489 "A multi buffer was expected to active after adding"
20490 );
20491 active_item.item_id()
20492 })
20493 .unwrap();
20494 cx.executor().run_until_parked();
20495
20496 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20497 editor.change_selections(
20498 SelectionEffects::scroll(Autoscroll::Next),
20499 window,
20500 cx,
20501 |s| s.select_ranges(Some(MultiBufferOffset(1)..MultiBufferOffset(2))),
20502 );
20503 editor.open_excerpts(&OpenExcerpts, window, cx);
20504 });
20505 cx.executor().run_until_parked();
20506 let first_item_id = workspace
20507 .update(cx, |workspace, window, cx| {
20508 let active_item = workspace
20509 .active_item(cx)
20510 .expect("should have an active item after navigating into the 1st buffer");
20511 let first_item_id = active_item.item_id();
20512 assert_ne!(
20513 first_item_id, multibuffer_item_id,
20514 "Should navigate into the 1st buffer and activate it"
20515 );
20516 assert_eq!(
20517 active_item.buffer_kind(cx),
20518 ItemBufferKind::Singleton,
20519 "New active item should be a singleton buffer"
20520 );
20521 assert_eq!(
20522 active_item
20523 .act_as::<Editor>(cx)
20524 .expect("should have navigated into an editor for the 1st buffer")
20525 .read(cx)
20526 .text(cx),
20527 sample_text_1
20528 );
20529
20530 workspace
20531 .go_back(workspace.active_pane().downgrade(), window, cx)
20532 .detach_and_log_err(cx);
20533
20534 first_item_id
20535 })
20536 .unwrap();
20537 cx.executor().run_until_parked();
20538 workspace
20539 .update(cx, |workspace, _, cx| {
20540 let active_item = workspace
20541 .active_item(cx)
20542 .expect("should have an active item after navigating back");
20543 assert_eq!(
20544 active_item.item_id(),
20545 multibuffer_item_id,
20546 "Should navigate back to the multi buffer"
20547 );
20548 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20549 })
20550 .unwrap();
20551
20552 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20553 editor.change_selections(
20554 SelectionEffects::scroll(Autoscroll::Next),
20555 window,
20556 cx,
20557 |s| s.select_ranges(Some(MultiBufferOffset(39)..MultiBufferOffset(40))),
20558 );
20559 editor.open_excerpts(&OpenExcerpts, window, cx);
20560 });
20561 cx.executor().run_until_parked();
20562 let second_item_id = workspace
20563 .update(cx, |workspace, window, cx| {
20564 let active_item = workspace
20565 .active_item(cx)
20566 .expect("should have an active item after navigating into the 2nd buffer");
20567 let second_item_id = active_item.item_id();
20568 assert_ne!(
20569 second_item_id, multibuffer_item_id,
20570 "Should navigate away from the multibuffer"
20571 );
20572 assert_ne!(
20573 second_item_id, first_item_id,
20574 "Should navigate into the 2nd buffer and activate it"
20575 );
20576 assert_eq!(
20577 active_item.buffer_kind(cx),
20578 ItemBufferKind::Singleton,
20579 "New active item should be a singleton buffer"
20580 );
20581 assert_eq!(
20582 active_item
20583 .act_as::<Editor>(cx)
20584 .expect("should have navigated into an editor")
20585 .read(cx)
20586 .text(cx),
20587 sample_text_2
20588 );
20589
20590 workspace
20591 .go_back(workspace.active_pane().downgrade(), window, cx)
20592 .detach_and_log_err(cx);
20593
20594 second_item_id
20595 })
20596 .unwrap();
20597 cx.executor().run_until_parked();
20598 workspace
20599 .update(cx, |workspace, _, cx| {
20600 let active_item = workspace
20601 .active_item(cx)
20602 .expect("should have an active item after navigating back from the 2nd buffer");
20603 assert_eq!(
20604 active_item.item_id(),
20605 multibuffer_item_id,
20606 "Should navigate back from the 2nd buffer to the multi buffer"
20607 );
20608 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20609 })
20610 .unwrap();
20611
20612 multi_buffer_editor.update_in(cx, |editor, window, cx| {
20613 editor.change_selections(
20614 SelectionEffects::scroll(Autoscroll::Next),
20615 window,
20616 cx,
20617 |s| s.select_ranges(Some(MultiBufferOffset(70)..MultiBufferOffset(70))),
20618 );
20619 editor.open_excerpts(&OpenExcerpts, window, cx);
20620 });
20621 cx.executor().run_until_parked();
20622 workspace
20623 .update(cx, |workspace, window, cx| {
20624 let active_item = workspace
20625 .active_item(cx)
20626 .expect("should have an active item after navigating into the 3rd buffer");
20627 let third_item_id = active_item.item_id();
20628 assert_ne!(
20629 third_item_id, multibuffer_item_id,
20630 "Should navigate into the 3rd buffer and activate it"
20631 );
20632 assert_ne!(third_item_id, first_item_id);
20633 assert_ne!(third_item_id, second_item_id);
20634 assert_eq!(
20635 active_item.buffer_kind(cx),
20636 ItemBufferKind::Singleton,
20637 "New active item should be a singleton buffer"
20638 );
20639 assert_eq!(
20640 active_item
20641 .act_as::<Editor>(cx)
20642 .expect("should have navigated into an editor")
20643 .read(cx)
20644 .text(cx),
20645 sample_text_3
20646 );
20647
20648 workspace
20649 .go_back(workspace.active_pane().downgrade(), window, cx)
20650 .detach_and_log_err(cx);
20651 })
20652 .unwrap();
20653 cx.executor().run_until_parked();
20654 workspace
20655 .update(cx, |workspace, _, cx| {
20656 let active_item = workspace
20657 .active_item(cx)
20658 .expect("should have an active item after navigating back from the 3rd buffer");
20659 assert_eq!(
20660 active_item.item_id(),
20661 multibuffer_item_id,
20662 "Should navigate back from the 3rd buffer to the multi buffer"
20663 );
20664 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
20665 })
20666 .unwrap();
20667}
20668
20669#[gpui::test]
20670async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20671 init_test(cx, |_| {});
20672
20673 let mut cx = EditorTestContext::new(cx).await;
20674
20675 let diff_base = r#"
20676 use some::mod;
20677
20678 const A: u32 = 42;
20679
20680 fn main() {
20681 println!("hello");
20682
20683 println!("world");
20684 }
20685 "#
20686 .unindent();
20687
20688 cx.set_state(
20689 &r#"
20690 use some::modified;
20691
20692 ˇ
20693 fn main() {
20694 println!("hello there");
20695
20696 println!("around the");
20697 println!("world");
20698 }
20699 "#
20700 .unindent(),
20701 );
20702
20703 cx.set_head_text(&diff_base);
20704 executor.run_until_parked();
20705
20706 cx.update_editor(|editor, window, cx| {
20707 editor.go_to_next_hunk(&GoToHunk, window, cx);
20708 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20709 });
20710 executor.run_until_parked();
20711 cx.assert_state_with_diff(
20712 r#"
20713 use some::modified;
20714
20715
20716 fn main() {
20717 - println!("hello");
20718 + ˇ println!("hello there");
20719
20720 println!("around the");
20721 println!("world");
20722 }
20723 "#
20724 .unindent(),
20725 );
20726
20727 cx.update_editor(|editor, window, cx| {
20728 for _ in 0..2 {
20729 editor.go_to_next_hunk(&GoToHunk, window, cx);
20730 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20731 }
20732 });
20733 executor.run_until_parked();
20734 cx.assert_state_with_diff(
20735 r#"
20736 - use some::mod;
20737 + ˇuse some::modified;
20738
20739
20740 fn main() {
20741 - println!("hello");
20742 + println!("hello there");
20743
20744 + println!("around the");
20745 println!("world");
20746 }
20747 "#
20748 .unindent(),
20749 );
20750
20751 cx.update_editor(|editor, window, cx| {
20752 editor.go_to_next_hunk(&GoToHunk, window, cx);
20753 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20754 });
20755 executor.run_until_parked();
20756 cx.assert_state_with_diff(
20757 r#"
20758 - use some::mod;
20759 + use some::modified;
20760
20761 - const A: u32 = 42;
20762 ˇ
20763 fn main() {
20764 - println!("hello");
20765 + println!("hello there");
20766
20767 + println!("around the");
20768 println!("world");
20769 }
20770 "#
20771 .unindent(),
20772 );
20773
20774 cx.update_editor(|editor, window, cx| {
20775 editor.cancel(&Cancel, window, cx);
20776 });
20777
20778 cx.assert_state_with_diff(
20779 r#"
20780 use some::modified;
20781
20782 ˇ
20783 fn main() {
20784 println!("hello there");
20785
20786 println!("around the");
20787 println!("world");
20788 }
20789 "#
20790 .unindent(),
20791 );
20792}
20793
20794#[gpui::test]
20795async fn test_diff_base_change_with_expanded_diff_hunks(
20796 executor: BackgroundExecutor,
20797 cx: &mut TestAppContext,
20798) {
20799 init_test(cx, |_| {});
20800
20801 let mut cx = EditorTestContext::new(cx).await;
20802
20803 let diff_base = r#"
20804 use some::mod1;
20805 use some::mod2;
20806
20807 const A: u32 = 42;
20808 const B: u32 = 42;
20809 const C: u32 = 42;
20810
20811 fn main() {
20812 println!("hello");
20813
20814 println!("world");
20815 }
20816 "#
20817 .unindent();
20818
20819 cx.set_state(
20820 &r#"
20821 use some::mod2;
20822
20823 const A: u32 = 42;
20824 const C: u32 = 42;
20825
20826 fn main(ˇ) {
20827 //println!("hello");
20828
20829 println!("world");
20830 //
20831 //
20832 }
20833 "#
20834 .unindent(),
20835 );
20836
20837 cx.set_head_text(&diff_base);
20838 executor.run_until_parked();
20839
20840 cx.update_editor(|editor, window, cx| {
20841 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20842 });
20843 executor.run_until_parked();
20844 cx.assert_state_with_diff(
20845 r#"
20846 - use some::mod1;
20847 use some::mod2;
20848
20849 const A: u32 = 42;
20850 - const B: u32 = 42;
20851 const C: u32 = 42;
20852
20853 fn main(ˇ) {
20854 - println!("hello");
20855 + //println!("hello");
20856
20857 println!("world");
20858 + //
20859 + //
20860 }
20861 "#
20862 .unindent(),
20863 );
20864
20865 cx.set_head_text("new diff base!");
20866 executor.run_until_parked();
20867 cx.assert_state_with_diff(
20868 r#"
20869 - new diff base!
20870 + use some::mod2;
20871 +
20872 + const A: u32 = 42;
20873 + const C: u32 = 42;
20874 +
20875 + fn main(ˇ) {
20876 + //println!("hello");
20877 +
20878 + println!("world");
20879 + //
20880 + //
20881 + }
20882 "#
20883 .unindent(),
20884 );
20885}
20886
20887#[gpui::test]
20888async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
20889 init_test(cx, |_| {});
20890
20891 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20892 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
20893 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20894 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
20895 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
20896 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
20897
20898 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
20899 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
20900 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
20901
20902 let multi_buffer = cx.new(|cx| {
20903 let mut multibuffer = MultiBuffer::new(ReadWrite);
20904 multibuffer.push_excerpts(
20905 buffer_1.clone(),
20906 [
20907 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20908 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20909 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20910 ],
20911 cx,
20912 );
20913 multibuffer.push_excerpts(
20914 buffer_2.clone(),
20915 [
20916 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20917 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20918 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20919 ],
20920 cx,
20921 );
20922 multibuffer.push_excerpts(
20923 buffer_3.clone(),
20924 [
20925 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
20926 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
20927 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
20928 ],
20929 cx,
20930 );
20931 multibuffer
20932 });
20933
20934 let editor =
20935 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
20936 editor
20937 .update(cx, |editor, _window, cx| {
20938 for (buffer, diff_base) in [
20939 (buffer_1.clone(), file_1_old),
20940 (buffer_2.clone(), file_2_old),
20941 (buffer_3.clone(), file_3_old),
20942 ] {
20943 let diff = cx.new(|cx| {
20944 BufferDiff::new_with_base_text(diff_base, &buffer.read(cx).text_snapshot(), cx)
20945 });
20946 editor
20947 .buffer
20948 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
20949 }
20950 })
20951 .unwrap();
20952
20953 let mut cx = EditorTestContext::for_editor(editor, cx).await;
20954 cx.run_until_parked();
20955
20956 cx.assert_editor_state(
20957 &"
20958 ˇaaa
20959 ccc
20960 ddd
20961
20962 ggg
20963 hhh
20964
20965
20966 lll
20967 mmm
20968 NNN
20969
20970 qqq
20971 rrr
20972
20973 uuu
20974 111
20975 222
20976 333
20977
20978 666
20979 777
20980
20981 000
20982 !!!"
20983 .unindent(),
20984 );
20985
20986 cx.update_editor(|editor, window, cx| {
20987 editor.select_all(&SelectAll, window, cx);
20988 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
20989 });
20990 cx.executor().run_until_parked();
20991
20992 cx.assert_state_with_diff(
20993 "
20994 «aaa
20995 - bbb
20996 ccc
20997 ddd
20998
20999 ggg
21000 hhh
21001
21002
21003 lll
21004 mmm
21005 - nnn
21006 + NNN
21007
21008 qqq
21009 rrr
21010
21011 uuu
21012 111
21013 222
21014 333
21015
21016 + 666
21017 777
21018
21019 000
21020 !!!ˇ»"
21021 .unindent(),
21022 );
21023}
21024
21025#[gpui::test]
21026async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
21027 init_test(cx, |_| {});
21028
21029 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
21030 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
21031
21032 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
21033 let multi_buffer = cx.new(|cx| {
21034 let mut multibuffer = MultiBuffer::new(ReadWrite);
21035 multibuffer.push_excerpts(
21036 buffer.clone(),
21037 [
21038 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
21039 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
21040 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
21041 ],
21042 cx,
21043 );
21044 multibuffer
21045 });
21046
21047 let editor =
21048 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
21049 editor
21050 .update(cx, |editor, _window, cx| {
21051 let diff = cx.new(|cx| {
21052 BufferDiff::new_with_base_text(base, &buffer.read(cx).text_snapshot(), cx)
21053 });
21054 editor
21055 .buffer
21056 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
21057 })
21058 .unwrap();
21059
21060 let mut cx = EditorTestContext::for_editor(editor, cx).await;
21061 cx.run_until_parked();
21062
21063 cx.update_editor(|editor, window, cx| {
21064 editor.expand_all_diff_hunks(&Default::default(), window, cx)
21065 });
21066 cx.executor().run_until_parked();
21067
21068 // When the start of a hunk coincides with the start of its excerpt,
21069 // the hunk is expanded. When the start of a hunk is earlier than
21070 // the start of its excerpt, the hunk is not expanded.
21071 cx.assert_state_with_diff(
21072 "
21073 ˇaaa
21074 - bbb
21075 + BBB
21076
21077 - ddd
21078 - eee
21079 + DDD
21080 + EEE
21081 fff
21082
21083 iii
21084 "
21085 .unindent(),
21086 );
21087}
21088
21089#[gpui::test]
21090async fn test_edits_around_expanded_insertion_hunks(
21091 executor: BackgroundExecutor,
21092 cx: &mut TestAppContext,
21093) {
21094 init_test(cx, |_| {});
21095
21096 let mut cx = EditorTestContext::new(cx).await;
21097
21098 let diff_base = r#"
21099 use some::mod1;
21100 use some::mod2;
21101
21102 const A: u32 = 42;
21103
21104 fn main() {
21105 println!("hello");
21106
21107 println!("world");
21108 }
21109 "#
21110 .unindent();
21111 executor.run_until_parked();
21112 cx.set_state(
21113 &r#"
21114 use some::mod1;
21115 use some::mod2;
21116
21117 const A: u32 = 42;
21118 const B: u32 = 42;
21119 const C: u32 = 42;
21120 ˇ
21121
21122 fn main() {
21123 println!("hello");
21124
21125 println!("world");
21126 }
21127 "#
21128 .unindent(),
21129 );
21130
21131 cx.set_head_text(&diff_base);
21132 executor.run_until_parked();
21133
21134 cx.update_editor(|editor, window, cx| {
21135 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21136 });
21137 executor.run_until_parked();
21138
21139 cx.assert_state_with_diff(
21140 r#"
21141 use some::mod1;
21142 use some::mod2;
21143
21144 const A: u32 = 42;
21145 + const B: u32 = 42;
21146 + const C: u32 = 42;
21147 + ˇ
21148
21149 fn main() {
21150 println!("hello");
21151
21152 println!("world");
21153 }
21154 "#
21155 .unindent(),
21156 );
21157
21158 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
21159 executor.run_until_parked();
21160
21161 cx.assert_state_with_diff(
21162 r#"
21163 use some::mod1;
21164 use some::mod2;
21165
21166 const A: u32 = 42;
21167 + const B: u32 = 42;
21168 + const C: u32 = 42;
21169 + const D: u32 = 42;
21170 + ˇ
21171
21172 fn main() {
21173 println!("hello");
21174
21175 println!("world");
21176 }
21177 "#
21178 .unindent(),
21179 );
21180
21181 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
21182 executor.run_until_parked();
21183
21184 cx.assert_state_with_diff(
21185 r#"
21186 use some::mod1;
21187 use some::mod2;
21188
21189 const A: u32 = 42;
21190 + const B: u32 = 42;
21191 + const C: u32 = 42;
21192 + const D: u32 = 42;
21193 + const E: u32 = 42;
21194 + ˇ
21195
21196 fn main() {
21197 println!("hello");
21198
21199 println!("world");
21200 }
21201 "#
21202 .unindent(),
21203 );
21204
21205 cx.update_editor(|editor, window, cx| {
21206 editor.delete_line(&DeleteLine, window, cx);
21207 });
21208 executor.run_until_parked();
21209
21210 cx.assert_state_with_diff(
21211 r#"
21212 use some::mod1;
21213 use some::mod2;
21214
21215 const A: u32 = 42;
21216 + const B: u32 = 42;
21217 + const C: u32 = 42;
21218 + const D: u32 = 42;
21219 + const E: u32 = 42;
21220 ˇ
21221 fn main() {
21222 println!("hello");
21223
21224 println!("world");
21225 }
21226 "#
21227 .unindent(),
21228 );
21229
21230 cx.update_editor(|editor, window, cx| {
21231 editor.move_up(&MoveUp, window, cx);
21232 editor.delete_line(&DeleteLine, window, cx);
21233 editor.move_up(&MoveUp, window, cx);
21234 editor.delete_line(&DeleteLine, window, cx);
21235 editor.move_up(&MoveUp, window, cx);
21236 editor.delete_line(&DeleteLine, window, cx);
21237 });
21238 executor.run_until_parked();
21239 cx.assert_state_with_diff(
21240 r#"
21241 use some::mod1;
21242 use some::mod2;
21243
21244 const A: u32 = 42;
21245 + const B: u32 = 42;
21246 ˇ
21247 fn main() {
21248 println!("hello");
21249
21250 println!("world");
21251 }
21252 "#
21253 .unindent(),
21254 );
21255
21256 cx.update_editor(|editor, window, cx| {
21257 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
21258 editor.delete_line(&DeleteLine, window, cx);
21259 });
21260 executor.run_until_parked();
21261 cx.assert_state_with_diff(
21262 r#"
21263 ˇ
21264 fn main() {
21265 println!("hello");
21266
21267 println!("world");
21268 }
21269 "#
21270 .unindent(),
21271 );
21272}
21273
21274#[gpui::test]
21275async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
21276 init_test(cx, |_| {});
21277
21278 let mut cx = EditorTestContext::new(cx).await;
21279 cx.set_head_text(indoc! { "
21280 one
21281 two
21282 three
21283 four
21284 five
21285 "
21286 });
21287 cx.set_state(indoc! { "
21288 one
21289 ˇthree
21290 five
21291 "});
21292 cx.run_until_parked();
21293 cx.update_editor(|editor, window, cx| {
21294 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21295 });
21296 cx.assert_state_with_diff(
21297 indoc! { "
21298 one
21299 - two
21300 ˇthree
21301 - four
21302 five
21303 "}
21304 .to_string(),
21305 );
21306 cx.update_editor(|editor, window, cx| {
21307 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21308 });
21309
21310 cx.assert_state_with_diff(
21311 indoc! { "
21312 one
21313 ˇthree
21314 five
21315 "}
21316 .to_string(),
21317 );
21318
21319 cx.update_editor(|editor, window, cx| {
21320 editor.move_up(&MoveUp, window, cx);
21321 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21322 });
21323 cx.assert_state_with_diff(
21324 indoc! { "
21325 ˇone
21326 - two
21327 three
21328 five
21329 "}
21330 .to_string(),
21331 );
21332
21333 cx.update_editor(|editor, window, cx| {
21334 editor.move_down(&MoveDown, window, cx);
21335 editor.move_down(&MoveDown, window, cx);
21336 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21337 });
21338 cx.assert_state_with_diff(
21339 indoc! { "
21340 one
21341 - two
21342 ˇthree
21343 - four
21344 five
21345 "}
21346 .to_string(),
21347 );
21348
21349 cx.set_state(indoc! { "
21350 one
21351 ˇTWO
21352 three
21353 four
21354 five
21355 "});
21356 cx.run_until_parked();
21357 cx.update_editor(|editor, window, cx| {
21358 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21359 });
21360
21361 cx.assert_state_with_diff(
21362 indoc! { "
21363 one
21364 - two
21365 + ˇTWO
21366 three
21367 four
21368 five
21369 "}
21370 .to_string(),
21371 );
21372 cx.update_editor(|editor, window, cx| {
21373 editor.move_up(&Default::default(), window, cx);
21374 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
21375 });
21376 cx.assert_state_with_diff(
21377 indoc! { "
21378 one
21379 ˇTWO
21380 three
21381 four
21382 five
21383 "}
21384 .to_string(),
21385 );
21386}
21387
21388#[gpui::test]
21389async fn test_toggling_adjacent_diff_hunks_2(
21390 executor: BackgroundExecutor,
21391 cx: &mut TestAppContext,
21392) {
21393 init_test(cx, |_| {});
21394
21395 let mut cx = EditorTestContext::new(cx).await;
21396
21397 let diff_base = r#"
21398 lineA
21399 lineB
21400 lineC
21401 lineD
21402 "#
21403 .unindent();
21404
21405 cx.set_state(
21406 &r#"
21407 ˇlineA1
21408 lineB
21409 lineD
21410 "#
21411 .unindent(),
21412 );
21413 cx.set_head_text(&diff_base);
21414 executor.run_until_parked();
21415
21416 cx.update_editor(|editor, window, cx| {
21417 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
21418 });
21419 executor.run_until_parked();
21420 cx.assert_state_with_diff(
21421 r#"
21422 - lineA
21423 + ˇlineA1
21424 lineB
21425 lineD
21426 "#
21427 .unindent(),
21428 );
21429
21430 cx.update_editor(|editor, window, cx| {
21431 editor.move_down(&MoveDown, window, cx);
21432 editor.move_right(&MoveRight, window, cx);
21433 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
21434 });
21435 executor.run_until_parked();
21436 cx.assert_state_with_diff(
21437 r#"
21438 - lineA
21439 + lineA1
21440 lˇineB
21441 - lineC
21442 lineD
21443 "#
21444 .unindent(),
21445 );
21446}
21447
21448#[gpui::test]
21449async fn test_edits_around_expanded_deletion_hunks(
21450 executor: BackgroundExecutor,
21451 cx: &mut TestAppContext,
21452) {
21453 init_test(cx, |_| {});
21454
21455 let mut cx = EditorTestContext::new(cx).await;
21456
21457 let diff_base = r#"
21458 use some::mod1;
21459 use some::mod2;
21460
21461 const A: u32 = 42;
21462 const B: u32 = 42;
21463 const C: u32 = 42;
21464
21465
21466 fn main() {
21467 println!("hello");
21468
21469 println!("world");
21470 }
21471 "#
21472 .unindent();
21473 executor.run_until_parked();
21474 cx.set_state(
21475 &r#"
21476 use some::mod1;
21477 use some::mod2;
21478
21479 ˇconst B: u32 = 42;
21480 const C: u32 = 42;
21481
21482
21483 fn main() {
21484 println!("hello");
21485
21486 println!("world");
21487 }
21488 "#
21489 .unindent(),
21490 );
21491
21492 cx.set_head_text(&diff_base);
21493 executor.run_until_parked();
21494
21495 cx.update_editor(|editor, window, cx| {
21496 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21497 });
21498 executor.run_until_parked();
21499
21500 cx.assert_state_with_diff(
21501 r#"
21502 use some::mod1;
21503 use some::mod2;
21504
21505 - const A: u32 = 42;
21506 ˇconst B: u32 = 42;
21507 const C: u32 = 42;
21508
21509
21510 fn main() {
21511 println!("hello");
21512
21513 println!("world");
21514 }
21515 "#
21516 .unindent(),
21517 );
21518
21519 cx.update_editor(|editor, window, cx| {
21520 editor.delete_line(&DeleteLine, window, cx);
21521 });
21522 executor.run_until_parked();
21523 cx.assert_state_with_diff(
21524 r#"
21525 use some::mod1;
21526 use some::mod2;
21527
21528 - const A: u32 = 42;
21529 - const B: u32 = 42;
21530 ˇconst C: u32 = 42;
21531
21532
21533 fn main() {
21534 println!("hello");
21535
21536 println!("world");
21537 }
21538 "#
21539 .unindent(),
21540 );
21541
21542 cx.update_editor(|editor, window, cx| {
21543 editor.delete_line(&DeleteLine, window, cx);
21544 });
21545 executor.run_until_parked();
21546 cx.assert_state_with_diff(
21547 r#"
21548 use some::mod1;
21549 use some::mod2;
21550
21551 - const A: u32 = 42;
21552 - const B: u32 = 42;
21553 - const C: u32 = 42;
21554 ˇ
21555
21556 fn main() {
21557 println!("hello");
21558
21559 println!("world");
21560 }
21561 "#
21562 .unindent(),
21563 );
21564
21565 cx.update_editor(|editor, window, cx| {
21566 editor.handle_input("replacement", window, cx);
21567 });
21568 executor.run_until_parked();
21569 cx.assert_state_with_diff(
21570 r#"
21571 use some::mod1;
21572 use some::mod2;
21573
21574 - const A: u32 = 42;
21575 - const B: u32 = 42;
21576 - const C: u32 = 42;
21577 -
21578 + replacementˇ
21579
21580 fn main() {
21581 println!("hello");
21582
21583 println!("world");
21584 }
21585 "#
21586 .unindent(),
21587 );
21588}
21589
21590#[gpui::test]
21591async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21592 init_test(cx, |_| {});
21593
21594 let mut cx = EditorTestContext::new(cx).await;
21595
21596 let base_text = r#"
21597 one
21598 two
21599 three
21600 four
21601 five
21602 "#
21603 .unindent();
21604 executor.run_until_parked();
21605 cx.set_state(
21606 &r#"
21607 one
21608 two
21609 fˇour
21610 five
21611 "#
21612 .unindent(),
21613 );
21614
21615 cx.set_head_text(&base_text);
21616 executor.run_until_parked();
21617
21618 cx.update_editor(|editor, window, cx| {
21619 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21620 });
21621 executor.run_until_parked();
21622
21623 cx.assert_state_with_diff(
21624 r#"
21625 one
21626 two
21627 - three
21628 fˇour
21629 five
21630 "#
21631 .unindent(),
21632 );
21633
21634 cx.update_editor(|editor, window, cx| {
21635 editor.backspace(&Backspace, window, cx);
21636 editor.backspace(&Backspace, window, cx);
21637 });
21638 executor.run_until_parked();
21639 cx.assert_state_with_diff(
21640 r#"
21641 one
21642 two
21643 - threeˇ
21644 - four
21645 + our
21646 five
21647 "#
21648 .unindent(),
21649 );
21650}
21651
21652#[gpui::test]
21653async fn test_edit_after_expanded_modification_hunk(
21654 executor: BackgroundExecutor,
21655 cx: &mut TestAppContext,
21656) {
21657 init_test(cx, |_| {});
21658
21659 let mut cx = EditorTestContext::new(cx).await;
21660
21661 let diff_base = r#"
21662 use some::mod1;
21663 use some::mod2;
21664
21665 const A: u32 = 42;
21666 const B: u32 = 42;
21667 const C: u32 = 42;
21668 const D: u32 = 42;
21669
21670
21671 fn main() {
21672 println!("hello");
21673
21674 println!("world");
21675 }"#
21676 .unindent();
21677
21678 cx.set_state(
21679 &r#"
21680 use some::mod1;
21681 use some::mod2;
21682
21683 const A: u32 = 42;
21684 const B: u32 = 42;
21685 const C: u32 = 43ˇ
21686 const D: u32 = 42;
21687
21688
21689 fn main() {
21690 println!("hello");
21691
21692 println!("world");
21693 }"#
21694 .unindent(),
21695 );
21696
21697 cx.set_head_text(&diff_base);
21698 executor.run_until_parked();
21699 cx.update_editor(|editor, window, cx| {
21700 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21701 });
21702 executor.run_until_parked();
21703
21704 cx.assert_state_with_diff(
21705 r#"
21706 use some::mod1;
21707 use some::mod2;
21708
21709 const A: u32 = 42;
21710 const B: u32 = 42;
21711 - const C: u32 = 42;
21712 + const C: u32 = 43ˇ
21713 const D: u32 = 42;
21714
21715
21716 fn main() {
21717 println!("hello");
21718
21719 println!("world");
21720 }"#
21721 .unindent(),
21722 );
21723
21724 cx.update_editor(|editor, window, cx| {
21725 editor.handle_input("\nnew_line\n", window, cx);
21726 });
21727 executor.run_until_parked();
21728
21729 cx.assert_state_with_diff(
21730 r#"
21731 use some::mod1;
21732 use some::mod2;
21733
21734 const A: u32 = 42;
21735 const B: u32 = 42;
21736 - const C: u32 = 42;
21737 + const C: u32 = 43
21738 + new_line
21739 + ˇ
21740 const D: u32 = 42;
21741
21742
21743 fn main() {
21744 println!("hello");
21745
21746 println!("world");
21747 }"#
21748 .unindent(),
21749 );
21750}
21751
21752#[gpui::test]
21753async fn test_stage_and_unstage_added_file_hunk(
21754 executor: BackgroundExecutor,
21755 cx: &mut TestAppContext,
21756) {
21757 init_test(cx, |_| {});
21758
21759 let mut cx = EditorTestContext::new(cx).await;
21760 cx.update_editor(|editor, _, cx| {
21761 editor.set_expand_all_diff_hunks(cx);
21762 });
21763
21764 let working_copy = r#"
21765 ˇfn main() {
21766 println!("hello, world!");
21767 }
21768 "#
21769 .unindent();
21770
21771 cx.set_state(&working_copy);
21772 executor.run_until_parked();
21773
21774 cx.assert_state_with_diff(
21775 r#"
21776 + ˇfn main() {
21777 + println!("hello, world!");
21778 + }
21779 "#
21780 .unindent(),
21781 );
21782 cx.assert_index_text(None);
21783
21784 cx.update_editor(|editor, window, cx| {
21785 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21786 });
21787 executor.run_until_parked();
21788 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
21789 cx.assert_state_with_diff(
21790 r#"
21791 + ˇfn main() {
21792 + println!("hello, world!");
21793 + }
21794 "#
21795 .unindent(),
21796 );
21797
21798 cx.update_editor(|editor, window, cx| {
21799 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21800 });
21801 executor.run_until_parked();
21802 cx.assert_index_text(None);
21803}
21804
21805async fn setup_indent_guides_editor(
21806 text: &str,
21807 cx: &mut TestAppContext,
21808) -> (BufferId, EditorTestContext) {
21809 init_test(cx, |_| {});
21810
21811 let mut cx = EditorTestContext::new(cx).await;
21812
21813 let buffer_id = cx.update_editor(|editor, window, cx| {
21814 editor.set_text(text, window, cx);
21815 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
21816
21817 buffer_ids[0]
21818 });
21819
21820 (buffer_id, cx)
21821}
21822
21823fn assert_indent_guides(
21824 range: Range<u32>,
21825 expected: Vec<IndentGuide>,
21826 active_indices: Option<Vec<usize>>,
21827 cx: &mut EditorTestContext,
21828) {
21829 let indent_guides = cx.update_editor(|editor, window, cx| {
21830 let snapshot = editor.snapshot(window, cx).display_snapshot;
21831 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
21832 editor,
21833 MultiBufferRow(range.start)..MultiBufferRow(range.end),
21834 true,
21835 &snapshot,
21836 cx,
21837 );
21838
21839 indent_guides.sort_by(|a, b| {
21840 a.depth.cmp(&b.depth).then(
21841 a.start_row
21842 .cmp(&b.start_row)
21843 .then(a.end_row.cmp(&b.end_row)),
21844 )
21845 });
21846 indent_guides
21847 });
21848
21849 if let Some(expected) = active_indices {
21850 let active_indices = cx.update_editor(|editor, window, cx| {
21851 let snapshot = editor.snapshot(window, cx).display_snapshot;
21852 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
21853 });
21854
21855 assert_eq!(
21856 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
21857 expected,
21858 "Active indent guide indices do not match"
21859 );
21860 }
21861
21862 assert_eq!(indent_guides, expected, "Indent guides do not match");
21863}
21864
21865fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
21866 IndentGuide {
21867 buffer_id,
21868 start_row: MultiBufferRow(start_row),
21869 end_row: MultiBufferRow(end_row),
21870 depth,
21871 tab_size: 4,
21872 settings: IndentGuideSettings {
21873 enabled: true,
21874 line_width: 1,
21875 active_line_width: 1,
21876 coloring: IndentGuideColoring::default(),
21877 background_coloring: IndentGuideBackgroundColoring::default(),
21878 },
21879 }
21880}
21881
21882#[gpui::test]
21883async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
21884 let (buffer_id, mut cx) = setup_indent_guides_editor(
21885 &"
21886 fn main() {
21887 let a = 1;
21888 }"
21889 .unindent(),
21890 cx,
21891 )
21892 .await;
21893
21894 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
21895}
21896
21897#[gpui::test]
21898async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
21899 let (buffer_id, mut cx) = setup_indent_guides_editor(
21900 &"
21901 fn main() {
21902 let a = 1;
21903 let b = 2;
21904 }"
21905 .unindent(),
21906 cx,
21907 )
21908 .await;
21909
21910 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
21911}
21912
21913#[gpui::test]
21914async fn test_indent_guide_nested(cx: &mut TestAppContext) {
21915 let (buffer_id, mut cx) = setup_indent_guides_editor(
21916 &"
21917 fn main() {
21918 let a = 1;
21919 if a == 3 {
21920 let b = 2;
21921 } else {
21922 let c = 3;
21923 }
21924 }"
21925 .unindent(),
21926 cx,
21927 )
21928 .await;
21929
21930 assert_indent_guides(
21931 0..8,
21932 vec![
21933 indent_guide(buffer_id, 1, 6, 0),
21934 indent_guide(buffer_id, 3, 3, 1),
21935 indent_guide(buffer_id, 5, 5, 1),
21936 ],
21937 None,
21938 &mut cx,
21939 );
21940}
21941
21942#[gpui::test]
21943async fn test_indent_guide_tab(cx: &mut TestAppContext) {
21944 let (buffer_id, mut cx) = setup_indent_guides_editor(
21945 &"
21946 fn main() {
21947 let a = 1;
21948 let b = 2;
21949 let c = 3;
21950 }"
21951 .unindent(),
21952 cx,
21953 )
21954 .await;
21955
21956 assert_indent_guides(
21957 0..5,
21958 vec![
21959 indent_guide(buffer_id, 1, 3, 0),
21960 indent_guide(buffer_id, 2, 2, 1),
21961 ],
21962 None,
21963 &mut cx,
21964 );
21965}
21966
21967#[gpui::test]
21968async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
21969 let (buffer_id, mut cx) = setup_indent_guides_editor(
21970 &"
21971 fn main() {
21972 let a = 1;
21973
21974 let c = 3;
21975 }"
21976 .unindent(),
21977 cx,
21978 )
21979 .await;
21980
21981 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
21982}
21983
21984#[gpui::test]
21985async fn test_indent_guide_complex(cx: &mut TestAppContext) {
21986 let (buffer_id, mut cx) = setup_indent_guides_editor(
21987 &"
21988 fn main() {
21989 let a = 1;
21990
21991 let c = 3;
21992
21993 if a == 3 {
21994 let b = 2;
21995 } else {
21996 let c = 3;
21997 }
21998 }"
21999 .unindent(),
22000 cx,
22001 )
22002 .await;
22003
22004 assert_indent_guides(
22005 0..11,
22006 vec![
22007 indent_guide(buffer_id, 1, 9, 0),
22008 indent_guide(buffer_id, 6, 6, 1),
22009 indent_guide(buffer_id, 8, 8, 1),
22010 ],
22011 None,
22012 &mut cx,
22013 );
22014}
22015
22016#[gpui::test]
22017async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
22018 let (buffer_id, mut cx) = setup_indent_guides_editor(
22019 &"
22020 fn main() {
22021 let a = 1;
22022
22023 let c = 3;
22024
22025 if a == 3 {
22026 let b = 2;
22027 } else {
22028 let c = 3;
22029 }
22030 }"
22031 .unindent(),
22032 cx,
22033 )
22034 .await;
22035
22036 assert_indent_guides(
22037 1..11,
22038 vec![
22039 indent_guide(buffer_id, 1, 9, 0),
22040 indent_guide(buffer_id, 6, 6, 1),
22041 indent_guide(buffer_id, 8, 8, 1),
22042 ],
22043 None,
22044 &mut cx,
22045 );
22046}
22047
22048#[gpui::test]
22049async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
22050 let (buffer_id, mut cx) = setup_indent_guides_editor(
22051 &"
22052 fn main() {
22053 let a = 1;
22054
22055 let c = 3;
22056
22057 if a == 3 {
22058 let b = 2;
22059 } else {
22060 let c = 3;
22061 }
22062 }"
22063 .unindent(),
22064 cx,
22065 )
22066 .await;
22067
22068 assert_indent_guides(
22069 1..10,
22070 vec![
22071 indent_guide(buffer_id, 1, 9, 0),
22072 indent_guide(buffer_id, 6, 6, 1),
22073 indent_guide(buffer_id, 8, 8, 1),
22074 ],
22075 None,
22076 &mut cx,
22077 );
22078}
22079
22080#[gpui::test]
22081async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
22082 let (buffer_id, mut cx) = setup_indent_guides_editor(
22083 &"
22084 fn main() {
22085 if a {
22086 b(
22087 c,
22088 d,
22089 )
22090 } else {
22091 e(
22092 f
22093 )
22094 }
22095 }"
22096 .unindent(),
22097 cx,
22098 )
22099 .await;
22100
22101 assert_indent_guides(
22102 0..11,
22103 vec![
22104 indent_guide(buffer_id, 1, 10, 0),
22105 indent_guide(buffer_id, 2, 5, 1),
22106 indent_guide(buffer_id, 7, 9, 1),
22107 indent_guide(buffer_id, 3, 4, 2),
22108 indent_guide(buffer_id, 8, 8, 2),
22109 ],
22110 None,
22111 &mut cx,
22112 );
22113
22114 cx.update_editor(|editor, window, cx| {
22115 editor.fold_at(MultiBufferRow(2), window, cx);
22116 assert_eq!(
22117 editor.display_text(cx),
22118 "
22119 fn main() {
22120 if a {
22121 b(⋯
22122 )
22123 } else {
22124 e(
22125 f
22126 )
22127 }
22128 }"
22129 .unindent()
22130 );
22131 });
22132
22133 assert_indent_guides(
22134 0..11,
22135 vec![
22136 indent_guide(buffer_id, 1, 10, 0),
22137 indent_guide(buffer_id, 2, 5, 1),
22138 indent_guide(buffer_id, 7, 9, 1),
22139 indent_guide(buffer_id, 8, 8, 2),
22140 ],
22141 None,
22142 &mut cx,
22143 );
22144}
22145
22146#[gpui::test]
22147async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
22148 let (buffer_id, mut cx) = setup_indent_guides_editor(
22149 &"
22150 block1
22151 block2
22152 block3
22153 block4
22154 block2
22155 block1
22156 block1"
22157 .unindent(),
22158 cx,
22159 )
22160 .await;
22161
22162 assert_indent_guides(
22163 1..10,
22164 vec![
22165 indent_guide(buffer_id, 1, 4, 0),
22166 indent_guide(buffer_id, 2, 3, 1),
22167 indent_guide(buffer_id, 3, 3, 2),
22168 ],
22169 None,
22170 &mut cx,
22171 );
22172}
22173
22174#[gpui::test]
22175async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
22176 let (buffer_id, mut cx) = setup_indent_guides_editor(
22177 &"
22178 block1
22179 block2
22180 block3
22181
22182 block1
22183 block1"
22184 .unindent(),
22185 cx,
22186 )
22187 .await;
22188
22189 assert_indent_guides(
22190 0..6,
22191 vec![
22192 indent_guide(buffer_id, 1, 2, 0),
22193 indent_guide(buffer_id, 2, 2, 1),
22194 ],
22195 None,
22196 &mut cx,
22197 );
22198}
22199
22200#[gpui::test]
22201async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
22202 let (buffer_id, mut cx) = setup_indent_guides_editor(
22203 &"
22204 function component() {
22205 \treturn (
22206 \t\t\t
22207 \t\t<div>
22208 \t\t\t<abc></abc>
22209 \t\t</div>
22210 \t)
22211 }"
22212 .unindent(),
22213 cx,
22214 )
22215 .await;
22216
22217 assert_indent_guides(
22218 0..8,
22219 vec![
22220 indent_guide(buffer_id, 1, 6, 0),
22221 indent_guide(buffer_id, 2, 5, 1),
22222 indent_guide(buffer_id, 4, 4, 2),
22223 ],
22224 None,
22225 &mut cx,
22226 );
22227}
22228
22229#[gpui::test]
22230async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
22231 let (buffer_id, mut cx) = setup_indent_guides_editor(
22232 &"
22233 function component() {
22234 \treturn (
22235 \t
22236 \t\t<div>
22237 \t\t\t<abc></abc>
22238 \t\t</div>
22239 \t)
22240 }"
22241 .unindent(),
22242 cx,
22243 )
22244 .await;
22245
22246 assert_indent_guides(
22247 0..8,
22248 vec![
22249 indent_guide(buffer_id, 1, 6, 0),
22250 indent_guide(buffer_id, 2, 5, 1),
22251 indent_guide(buffer_id, 4, 4, 2),
22252 ],
22253 None,
22254 &mut cx,
22255 );
22256}
22257
22258#[gpui::test]
22259async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
22260 let (buffer_id, mut cx) = setup_indent_guides_editor(
22261 &"
22262 block1
22263
22264
22265
22266 block2
22267 "
22268 .unindent(),
22269 cx,
22270 )
22271 .await;
22272
22273 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
22274}
22275
22276#[gpui::test]
22277async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
22278 let (buffer_id, mut cx) = setup_indent_guides_editor(
22279 &"
22280 def a:
22281 \tb = 3
22282 \tif True:
22283 \t\tc = 4
22284 \t\td = 5
22285 \tprint(b)
22286 "
22287 .unindent(),
22288 cx,
22289 )
22290 .await;
22291
22292 assert_indent_guides(
22293 0..6,
22294 vec![
22295 indent_guide(buffer_id, 1, 5, 0),
22296 indent_guide(buffer_id, 3, 4, 1),
22297 ],
22298 None,
22299 &mut cx,
22300 );
22301}
22302
22303#[gpui::test]
22304async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
22305 let (buffer_id, mut cx) = setup_indent_guides_editor(
22306 &"
22307 fn main() {
22308 let a = 1;
22309 }"
22310 .unindent(),
22311 cx,
22312 )
22313 .await;
22314
22315 cx.update_editor(|editor, window, cx| {
22316 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22317 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
22318 });
22319 });
22320
22321 assert_indent_guides(
22322 0..3,
22323 vec![indent_guide(buffer_id, 1, 1, 0)],
22324 Some(vec![0]),
22325 &mut cx,
22326 );
22327}
22328
22329#[gpui::test]
22330async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
22331 let (buffer_id, mut cx) = setup_indent_guides_editor(
22332 &"
22333 fn main() {
22334 if 1 == 2 {
22335 let a = 1;
22336 }
22337 }"
22338 .unindent(),
22339 cx,
22340 )
22341 .await;
22342
22343 cx.update_editor(|editor, window, cx| {
22344 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22345 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
22346 });
22347 });
22348 cx.run_until_parked();
22349
22350 assert_indent_guides(
22351 0..4,
22352 vec![
22353 indent_guide(buffer_id, 1, 3, 0),
22354 indent_guide(buffer_id, 2, 2, 1),
22355 ],
22356 Some(vec![1]),
22357 &mut cx,
22358 );
22359
22360 cx.update_editor(|editor, window, cx| {
22361 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22362 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
22363 });
22364 });
22365 cx.run_until_parked();
22366
22367 assert_indent_guides(
22368 0..4,
22369 vec![
22370 indent_guide(buffer_id, 1, 3, 0),
22371 indent_guide(buffer_id, 2, 2, 1),
22372 ],
22373 Some(vec![1]),
22374 &mut cx,
22375 );
22376
22377 cx.update_editor(|editor, window, cx| {
22378 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22379 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
22380 });
22381 });
22382 cx.run_until_parked();
22383
22384 assert_indent_guides(
22385 0..4,
22386 vec![
22387 indent_guide(buffer_id, 1, 3, 0),
22388 indent_guide(buffer_id, 2, 2, 1),
22389 ],
22390 Some(vec![0]),
22391 &mut cx,
22392 );
22393}
22394
22395#[gpui::test]
22396async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
22397 let (buffer_id, mut cx) = setup_indent_guides_editor(
22398 &"
22399 fn main() {
22400 let a = 1;
22401
22402 let b = 2;
22403 }"
22404 .unindent(),
22405 cx,
22406 )
22407 .await;
22408
22409 cx.update_editor(|editor, window, cx| {
22410 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22411 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
22412 });
22413 });
22414
22415 assert_indent_guides(
22416 0..5,
22417 vec![indent_guide(buffer_id, 1, 3, 0)],
22418 Some(vec![0]),
22419 &mut cx,
22420 );
22421}
22422
22423#[gpui::test]
22424async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
22425 let (buffer_id, mut cx) = setup_indent_guides_editor(
22426 &"
22427 def m:
22428 a = 1
22429 pass"
22430 .unindent(),
22431 cx,
22432 )
22433 .await;
22434
22435 cx.update_editor(|editor, window, cx| {
22436 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22437 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
22438 });
22439 });
22440
22441 assert_indent_guides(
22442 0..3,
22443 vec![indent_guide(buffer_id, 1, 2, 0)],
22444 Some(vec![0]),
22445 &mut cx,
22446 );
22447}
22448
22449#[gpui::test]
22450async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
22451 init_test(cx, |_| {});
22452 let mut cx = EditorTestContext::new(cx).await;
22453 let text = indoc! {
22454 "
22455 impl A {
22456 fn b() {
22457 0;
22458 3;
22459 5;
22460 6;
22461 7;
22462 }
22463 }
22464 "
22465 };
22466 let base_text = indoc! {
22467 "
22468 impl A {
22469 fn b() {
22470 0;
22471 1;
22472 2;
22473 3;
22474 4;
22475 }
22476 fn c() {
22477 5;
22478 6;
22479 7;
22480 }
22481 }
22482 "
22483 };
22484
22485 cx.update_editor(|editor, window, cx| {
22486 editor.set_text(text, window, cx);
22487
22488 editor.buffer().update(cx, |multibuffer, cx| {
22489 let buffer = multibuffer.as_singleton().unwrap();
22490 let diff = cx.new(|cx| {
22491 BufferDiff::new_with_base_text(base_text, &buffer.read(cx).text_snapshot(), cx)
22492 });
22493
22494 multibuffer.set_all_diff_hunks_expanded(cx);
22495 multibuffer.add_diff(diff, cx);
22496
22497 buffer.read(cx).remote_id()
22498 })
22499 });
22500 cx.run_until_parked();
22501
22502 cx.assert_state_with_diff(
22503 indoc! { "
22504 impl A {
22505 fn b() {
22506 0;
22507 - 1;
22508 - 2;
22509 3;
22510 - 4;
22511 - }
22512 - fn c() {
22513 5;
22514 6;
22515 7;
22516 }
22517 }
22518 ˇ"
22519 }
22520 .to_string(),
22521 );
22522
22523 let mut actual_guides = cx.update_editor(|editor, window, cx| {
22524 editor
22525 .snapshot(window, cx)
22526 .buffer_snapshot()
22527 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
22528 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
22529 .collect::<Vec<_>>()
22530 });
22531 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
22532 assert_eq!(
22533 actual_guides,
22534 vec![
22535 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
22536 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
22537 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
22538 ]
22539 );
22540}
22541
22542#[gpui::test]
22543async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
22544 init_test(cx, |_| {});
22545 let mut cx = EditorTestContext::new(cx).await;
22546
22547 let diff_base = r#"
22548 a
22549 b
22550 c
22551 "#
22552 .unindent();
22553
22554 cx.set_state(
22555 &r#"
22556 ˇA
22557 b
22558 C
22559 "#
22560 .unindent(),
22561 );
22562 cx.set_head_text(&diff_base);
22563 cx.update_editor(|editor, window, cx| {
22564 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22565 });
22566 executor.run_until_parked();
22567
22568 let both_hunks_expanded = r#"
22569 - a
22570 + ˇA
22571 b
22572 - c
22573 + C
22574 "#
22575 .unindent();
22576
22577 cx.assert_state_with_diff(both_hunks_expanded.clone());
22578
22579 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22580 let snapshot = editor.snapshot(window, cx);
22581 let hunks = editor
22582 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22583 .collect::<Vec<_>>();
22584 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22585 hunks
22586 .into_iter()
22587 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22588 .collect::<Vec<_>>()
22589 });
22590 assert_eq!(hunk_ranges.len(), 2);
22591
22592 cx.update_editor(|editor, _, cx| {
22593 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22594 });
22595 executor.run_until_parked();
22596
22597 let second_hunk_expanded = r#"
22598 ˇA
22599 b
22600 - c
22601 + C
22602 "#
22603 .unindent();
22604
22605 cx.assert_state_with_diff(second_hunk_expanded);
22606
22607 cx.update_editor(|editor, _, cx| {
22608 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22609 });
22610 executor.run_until_parked();
22611
22612 cx.assert_state_with_diff(both_hunks_expanded.clone());
22613
22614 cx.update_editor(|editor, _, cx| {
22615 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22616 });
22617 executor.run_until_parked();
22618
22619 let first_hunk_expanded = r#"
22620 - a
22621 + ˇA
22622 b
22623 C
22624 "#
22625 .unindent();
22626
22627 cx.assert_state_with_diff(first_hunk_expanded);
22628
22629 cx.update_editor(|editor, _, cx| {
22630 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22631 });
22632 executor.run_until_parked();
22633
22634 cx.assert_state_with_diff(both_hunks_expanded);
22635
22636 cx.set_state(
22637 &r#"
22638 ˇA
22639 b
22640 "#
22641 .unindent(),
22642 );
22643 cx.run_until_parked();
22644
22645 // TODO this cursor position seems bad
22646 cx.assert_state_with_diff(
22647 r#"
22648 - ˇa
22649 + A
22650 b
22651 "#
22652 .unindent(),
22653 );
22654
22655 cx.update_editor(|editor, window, cx| {
22656 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22657 });
22658
22659 cx.assert_state_with_diff(
22660 r#"
22661 - ˇa
22662 + A
22663 b
22664 - c
22665 "#
22666 .unindent(),
22667 );
22668
22669 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22670 let snapshot = editor.snapshot(window, cx);
22671 let hunks = editor
22672 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22673 .collect::<Vec<_>>();
22674 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22675 hunks
22676 .into_iter()
22677 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22678 .collect::<Vec<_>>()
22679 });
22680 assert_eq!(hunk_ranges.len(), 2);
22681
22682 cx.update_editor(|editor, _, cx| {
22683 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
22684 });
22685 executor.run_until_parked();
22686
22687 cx.assert_state_with_diff(
22688 r#"
22689 - ˇa
22690 + A
22691 b
22692 "#
22693 .unindent(),
22694 );
22695}
22696
22697#[gpui::test]
22698async fn test_toggle_deletion_hunk_at_start_of_file(
22699 executor: BackgroundExecutor,
22700 cx: &mut TestAppContext,
22701) {
22702 init_test(cx, |_| {});
22703 let mut cx = EditorTestContext::new(cx).await;
22704
22705 let diff_base = r#"
22706 a
22707 b
22708 c
22709 "#
22710 .unindent();
22711
22712 cx.set_state(
22713 &r#"
22714 ˇb
22715 c
22716 "#
22717 .unindent(),
22718 );
22719 cx.set_head_text(&diff_base);
22720 cx.update_editor(|editor, window, cx| {
22721 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
22722 });
22723 executor.run_until_parked();
22724
22725 let hunk_expanded = r#"
22726 - a
22727 ˇb
22728 c
22729 "#
22730 .unindent();
22731
22732 cx.assert_state_with_diff(hunk_expanded.clone());
22733
22734 let hunk_ranges = cx.update_editor(|editor, window, cx| {
22735 let snapshot = editor.snapshot(window, cx);
22736 let hunks = editor
22737 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22738 .collect::<Vec<_>>();
22739 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22740 hunks
22741 .into_iter()
22742 .map(|hunk| Anchor::range_in_buffer(excerpt_id, hunk.buffer_range))
22743 .collect::<Vec<_>>()
22744 });
22745 assert_eq!(hunk_ranges.len(), 1);
22746
22747 cx.update_editor(|editor, _, cx| {
22748 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22749 });
22750 executor.run_until_parked();
22751
22752 let hunk_collapsed = r#"
22753 ˇb
22754 c
22755 "#
22756 .unindent();
22757
22758 cx.assert_state_with_diff(hunk_collapsed);
22759
22760 cx.update_editor(|editor, _, cx| {
22761 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
22762 });
22763 executor.run_until_parked();
22764
22765 cx.assert_state_with_diff(hunk_expanded);
22766}
22767
22768#[gpui::test]
22769async fn test_expand_first_line_diff_hunk_keeps_deleted_lines_visible(
22770 executor: BackgroundExecutor,
22771 cx: &mut TestAppContext,
22772) {
22773 init_test(cx, |_| {});
22774 let mut cx = EditorTestContext::new(cx).await;
22775
22776 cx.set_state("ˇnew\nsecond\nthird\n");
22777 cx.set_head_text("old\nsecond\nthird\n");
22778 cx.update_editor(|editor, window, cx| {
22779 editor.scroll(gpui::Point { x: 0., y: 0. }, None, window, cx);
22780 });
22781 executor.run_until_parked();
22782 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
22783
22784 // Expanding a diff hunk at the first line inserts deleted lines above the first buffer line.
22785 cx.update_editor(|editor, window, cx| {
22786 let snapshot = editor.snapshot(window, cx);
22787 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
22788 let hunks = editor
22789 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22790 .collect::<Vec<_>>();
22791 assert_eq!(hunks.len(), 1);
22792 let hunk_range = Anchor::range_in_buffer(excerpt_id, hunks[0].buffer_range.clone());
22793 editor.toggle_single_diff_hunk(hunk_range, cx)
22794 });
22795 executor.run_until_parked();
22796 cx.assert_state_with_diff("- old\n+ ˇnew\n second\n third\n".to_string());
22797
22798 // Keep the editor scrolled to the top so the full hunk remains visible.
22799 assert_eq!(cx.update_editor(|e, _, cx| e.scroll_position(cx)).y, 0.0);
22800}
22801
22802#[gpui::test]
22803async fn test_display_diff_hunks(cx: &mut TestAppContext) {
22804 init_test(cx, |_| {});
22805
22806 let fs = FakeFs::new(cx.executor());
22807 fs.insert_tree(
22808 path!("/test"),
22809 json!({
22810 ".git": {},
22811 "file-1": "ONE\n",
22812 "file-2": "TWO\n",
22813 "file-3": "THREE\n",
22814 }),
22815 )
22816 .await;
22817
22818 fs.set_head_for_repo(
22819 path!("/test/.git").as_ref(),
22820 &[
22821 ("file-1", "one\n".into()),
22822 ("file-2", "two\n".into()),
22823 ("file-3", "three\n".into()),
22824 ],
22825 "deadbeef",
22826 );
22827
22828 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
22829 let mut buffers = vec![];
22830 for i in 1..=3 {
22831 let buffer = project
22832 .update(cx, |project, cx| {
22833 let path = format!(path!("/test/file-{}"), i);
22834 project.open_local_buffer(path, cx)
22835 })
22836 .await
22837 .unwrap();
22838 buffers.push(buffer);
22839 }
22840
22841 let multibuffer = cx.new(|cx| {
22842 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
22843 multibuffer.set_all_diff_hunks_expanded(cx);
22844 for buffer in &buffers {
22845 let snapshot = buffer.read(cx).snapshot();
22846 multibuffer.set_excerpts_for_path(
22847 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
22848 buffer.clone(),
22849 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
22850 2,
22851 cx,
22852 );
22853 }
22854 multibuffer
22855 });
22856
22857 let editor = cx.add_window(|window, cx| {
22858 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
22859 });
22860 cx.run_until_parked();
22861
22862 let snapshot = editor
22863 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
22864 .unwrap();
22865 let hunks = snapshot
22866 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
22867 .map(|hunk| match hunk {
22868 DisplayDiffHunk::Unfolded {
22869 display_row_range, ..
22870 } => display_row_range,
22871 DisplayDiffHunk::Folded { .. } => unreachable!(),
22872 })
22873 .collect::<Vec<_>>();
22874 assert_eq!(
22875 hunks,
22876 [
22877 DisplayRow(2)..DisplayRow(4),
22878 DisplayRow(7)..DisplayRow(9),
22879 DisplayRow(12)..DisplayRow(14),
22880 ]
22881 );
22882}
22883
22884#[gpui::test]
22885async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
22886 init_test(cx, |_| {});
22887
22888 let mut cx = EditorTestContext::new(cx).await;
22889 cx.set_head_text(indoc! { "
22890 one
22891 two
22892 three
22893 four
22894 five
22895 "
22896 });
22897 cx.set_index_text(indoc! { "
22898 one
22899 two
22900 three
22901 four
22902 five
22903 "
22904 });
22905 cx.set_state(indoc! {"
22906 one
22907 TWO
22908 ˇTHREE
22909 FOUR
22910 five
22911 "});
22912 cx.run_until_parked();
22913 cx.update_editor(|editor, window, cx| {
22914 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22915 });
22916 cx.run_until_parked();
22917 cx.assert_index_text(Some(indoc! {"
22918 one
22919 TWO
22920 THREE
22921 FOUR
22922 five
22923 "}));
22924 cx.set_state(indoc! { "
22925 one
22926 TWO
22927 ˇTHREE-HUNDRED
22928 FOUR
22929 five
22930 "});
22931 cx.run_until_parked();
22932 cx.update_editor(|editor, window, cx| {
22933 let snapshot = editor.snapshot(window, cx);
22934 let hunks = editor
22935 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
22936 .collect::<Vec<_>>();
22937 assert_eq!(hunks.len(), 1);
22938 assert_eq!(
22939 hunks[0].status(),
22940 DiffHunkStatus {
22941 kind: DiffHunkStatusKind::Modified,
22942 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
22943 }
22944 );
22945
22946 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
22947 });
22948 cx.run_until_parked();
22949 cx.assert_index_text(Some(indoc! {"
22950 one
22951 TWO
22952 THREE-HUNDRED
22953 FOUR
22954 five
22955 "}));
22956}
22957
22958#[gpui::test]
22959fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
22960 init_test(cx, |_| {});
22961
22962 let editor = cx.add_window(|window, cx| {
22963 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
22964 build_editor(buffer, window, cx)
22965 });
22966
22967 let render_args = Arc::new(Mutex::new(None));
22968 let snapshot = editor
22969 .update(cx, |editor, window, cx| {
22970 let snapshot = editor.buffer().read(cx).snapshot(cx);
22971 let range =
22972 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
22973
22974 struct RenderArgs {
22975 row: MultiBufferRow,
22976 folded: bool,
22977 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
22978 }
22979
22980 let crease = Crease::inline(
22981 range,
22982 FoldPlaceholder::test(),
22983 {
22984 let toggle_callback = render_args.clone();
22985 move |row, folded, callback, _window, _cx| {
22986 *toggle_callback.lock() = Some(RenderArgs {
22987 row,
22988 folded,
22989 callback,
22990 });
22991 div()
22992 }
22993 },
22994 |_row, _folded, _window, _cx| div(),
22995 );
22996
22997 editor.insert_creases(Some(crease), cx);
22998 let snapshot = editor.snapshot(window, cx);
22999 let _div =
23000 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
23001 snapshot
23002 })
23003 .unwrap();
23004
23005 let render_args = render_args.lock().take().unwrap();
23006 assert_eq!(render_args.row, MultiBufferRow(1));
23007 assert!(!render_args.folded);
23008 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
23009
23010 cx.update_window(*editor, |_, window, cx| {
23011 (render_args.callback)(true, window, cx)
23012 })
23013 .unwrap();
23014 let snapshot = editor
23015 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
23016 .unwrap();
23017 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
23018
23019 cx.update_window(*editor, |_, window, cx| {
23020 (render_args.callback)(false, window, cx)
23021 })
23022 .unwrap();
23023 let snapshot = editor
23024 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
23025 .unwrap();
23026 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
23027}
23028
23029#[gpui::test]
23030async fn test_input_text(cx: &mut TestAppContext) {
23031 init_test(cx, |_| {});
23032 let mut cx = EditorTestContext::new(cx).await;
23033
23034 cx.set_state(
23035 &r#"ˇone
23036 two
23037
23038 three
23039 fourˇ
23040 five
23041
23042 siˇx"#
23043 .unindent(),
23044 );
23045
23046 cx.dispatch_action(HandleInput(String::new()));
23047 cx.assert_editor_state(
23048 &r#"ˇone
23049 two
23050
23051 three
23052 fourˇ
23053 five
23054
23055 siˇx"#
23056 .unindent(),
23057 );
23058
23059 cx.dispatch_action(HandleInput("AAAA".to_string()));
23060 cx.assert_editor_state(
23061 &r#"AAAAˇone
23062 two
23063
23064 three
23065 fourAAAAˇ
23066 five
23067
23068 siAAAAˇx"#
23069 .unindent(),
23070 );
23071}
23072
23073#[gpui::test]
23074async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
23075 init_test(cx, |_| {});
23076
23077 let mut cx = EditorTestContext::new(cx).await;
23078 cx.set_state(
23079 r#"let foo = 1;
23080let foo = 2;
23081let foo = 3;
23082let fooˇ = 4;
23083let foo = 5;
23084let foo = 6;
23085let foo = 7;
23086let foo = 8;
23087let foo = 9;
23088let foo = 10;
23089let foo = 11;
23090let foo = 12;
23091let foo = 13;
23092let foo = 14;
23093let foo = 15;"#,
23094 );
23095
23096 cx.update_editor(|e, window, cx| {
23097 assert_eq!(
23098 e.next_scroll_position,
23099 NextScrollCursorCenterTopBottom::Center,
23100 "Default next scroll direction is center",
23101 );
23102
23103 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
23104 assert_eq!(
23105 e.next_scroll_position,
23106 NextScrollCursorCenterTopBottom::Top,
23107 "After center, next scroll direction should be top",
23108 );
23109
23110 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
23111 assert_eq!(
23112 e.next_scroll_position,
23113 NextScrollCursorCenterTopBottom::Bottom,
23114 "After top, next scroll direction should be bottom",
23115 );
23116
23117 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
23118 assert_eq!(
23119 e.next_scroll_position,
23120 NextScrollCursorCenterTopBottom::Center,
23121 "After bottom, scrolling should start over",
23122 );
23123
23124 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
23125 assert_eq!(
23126 e.next_scroll_position,
23127 NextScrollCursorCenterTopBottom::Top,
23128 "Scrolling continues if retriggered fast enough"
23129 );
23130 });
23131
23132 cx.executor()
23133 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
23134 cx.executor().run_until_parked();
23135 cx.update_editor(|e, _, _| {
23136 assert_eq!(
23137 e.next_scroll_position,
23138 NextScrollCursorCenterTopBottom::Center,
23139 "If scrolling is not triggered fast enough, it should reset"
23140 );
23141 });
23142}
23143
23144#[gpui::test]
23145async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
23146 init_test(cx, |_| {});
23147 let mut cx = EditorLspTestContext::new_rust(
23148 lsp::ServerCapabilities {
23149 definition_provider: Some(lsp::OneOf::Left(true)),
23150 references_provider: Some(lsp::OneOf::Left(true)),
23151 ..lsp::ServerCapabilities::default()
23152 },
23153 cx,
23154 )
23155 .await;
23156
23157 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
23158 let go_to_definition = cx
23159 .lsp
23160 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
23161 move |params, _| async move {
23162 if empty_go_to_definition {
23163 Ok(None)
23164 } else {
23165 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
23166 uri: params.text_document_position_params.text_document.uri,
23167 range: lsp::Range::new(
23168 lsp::Position::new(4, 3),
23169 lsp::Position::new(4, 6),
23170 ),
23171 })))
23172 }
23173 },
23174 );
23175 let references = cx
23176 .lsp
23177 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
23178 Ok(Some(vec![lsp::Location {
23179 uri: params.text_document_position.text_document.uri,
23180 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
23181 }]))
23182 });
23183 (go_to_definition, references)
23184 };
23185
23186 cx.set_state(
23187 &r#"fn one() {
23188 let mut a = ˇtwo();
23189 }
23190
23191 fn two() {}"#
23192 .unindent(),
23193 );
23194 set_up_lsp_handlers(false, &mut cx);
23195 let navigated = cx
23196 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
23197 .await
23198 .expect("Failed to navigate to definition");
23199 assert_eq!(
23200 navigated,
23201 Navigated::Yes,
23202 "Should have navigated to definition from the GetDefinition response"
23203 );
23204 cx.assert_editor_state(
23205 &r#"fn one() {
23206 let mut a = two();
23207 }
23208
23209 fn «twoˇ»() {}"#
23210 .unindent(),
23211 );
23212
23213 let editors = cx.update_workspace(|workspace, _, cx| {
23214 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23215 });
23216 cx.update_editor(|_, _, test_editor_cx| {
23217 assert_eq!(
23218 editors.len(),
23219 1,
23220 "Initially, only one, test, editor should be open in the workspace"
23221 );
23222 assert_eq!(
23223 test_editor_cx.entity(),
23224 editors.last().expect("Asserted len is 1").clone()
23225 );
23226 });
23227
23228 set_up_lsp_handlers(true, &mut cx);
23229 let navigated = cx
23230 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
23231 .await
23232 .expect("Failed to navigate to lookup references");
23233 assert_eq!(
23234 navigated,
23235 Navigated::Yes,
23236 "Should have navigated to references as a fallback after empty GoToDefinition response"
23237 );
23238 // We should not change the selections in the existing file,
23239 // if opening another milti buffer with the references
23240 cx.assert_editor_state(
23241 &r#"fn one() {
23242 let mut a = two();
23243 }
23244
23245 fn «twoˇ»() {}"#
23246 .unindent(),
23247 );
23248 let editors = cx.update_workspace(|workspace, _, cx| {
23249 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23250 });
23251 cx.update_editor(|_, _, test_editor_cx| {
23252 assert_eq!(
23253 editors.len(),
23254 2,
23255 "After falling back to references search, we open a new editor with the results"
23256 );
23257 let references_fallback_text = editors
23258 .into_iter()
23259 .find(|new_editor| *new_editor != test_editor_cx.entity())
23260 .expect("Should have one non-test editor now")
23261 .read(test_editor_cx)
23262 .text(test_editor_cx);
23263 assert_eq!(
23264 references_fallback_text, "fn one() {\n let mut a = two();\n}",
23265 "Should use the range from the references response and not the GoToDefinition one"
23266 );
23267 });
23268}
23269
23270#[gpui::test]
23271async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
23272 init_test(cx, |_| {});
23273 cx.update(|cx| {
23274 let mut editor_settings = EditorSettings::get_global(cx).clone();
23275 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
23276 EditorSettings::override_global(editor_settings, cx);
23277 });
23278 let mut cx = EditorLspTestContext::new_rust(
23279 lsp::ServerCapabilities {
23280 definition_provider: Some(lsp::OneOf::Left(true)),
23281 references_provider: Some(lsp::OneOf::Left(true)),
23282 ..lsp::ServerCapabilities::default()
23283 },
23284 cx,
23285 )
23286 .await;
23287 let original_state = r#"fn one() {
23288 let mut a = ˇtwo();
23289 }
23290
23291 fn two() {}"#
23292 .unindent();
23293 cx.set_state(&original_state);
23294
23295 let mut go_to_definition = cx
23296 .lsp
23297 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
23298 move |_, _| async move { Ok(None) },
23299 );
23300 let _references = cx
23301 .lsp
23302 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
23303 panic!("Should not call for references with no go to definition fallback")
23304 });
23305
23306 let navigated = cx
23307 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
23308 .await
23309 .expect("Failed to navigate to lookup references");
23310 go_to_definition
23311 .next()
23312 .await
23313 .expect("Should have called the go_to_definition handler");
23314
23315 assert_eq!(
23316 navigated,
23317 Navigated::No,
23318 "Should have navigated to references as a fallback after empty GoToDefinition response"
23319 );
23320 cx.assert_editor_state(&original_state);
23321 let editors = cx.update_workspace(|workspace, _, cx| {
23322 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23323 });
23324 cx.update_editor(|_, _, _| {
23325 assert_eq!(
23326 editors.len(),
23327 1,
23328 "After unsuccessful fallback, no other editor should have been opened"
23329 );
23330 });
23331}
23332
23333#[gpui::test]
23334async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
23335 init_test(cx, |_| {});
23336 let mut cx = EditorLspTestContext::new_rust(
23337 lsp::ServerCapabilities {
23338 references_provider: Some(lsp::OneOf::Left(true)),
23339 ..lsp::ServerCapabilities::default()
23340 },
23341 cx,
23342 )
23343 .await;
23344
23345 cx.set_state(
23346 &r#"
23347 fn one() {
23348 let mut a = two();
23349 }
23350
23351 fn ˇtwo() {}"#
23352 .unindent(),
23353 );
23354 cx.lsp
23355 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
23356 Ok(Some(vec![
23357 lsp::Location {
23358 uri: params.text_document_position.text_document.uri.clone(),
23359 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
23360 },
23361 lsp::Location {
23362 uri: params.text_document_position.text_document.uri,
23363 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
23364 },
23365 ]))
23366 });
23367 let navigated = cx
23368 .update_editor(|editor, window, cx| {
23369 editor.find_all_references(&FindAllReferences::default(), window, cx)
23370 })
23371 .unwrap()
23372 .await
23373 .expect("Failed to navigate to references");
23374 assert_eq!(
23375 navigated,
23376 Navigated::Yes,
23377 "Should have navigated to references from the FindAllReferences response"
23378 );
23379 cx.assert_editor_state(
23380 &r#"fn one() {
23381 let mut a = two();
23382 }
23383
23384 fn ˇtwo() {}"#
23385 .unindent(),
23386 );
23387
23388 let editors = cx.update_workspace(|workspace, _, cx| {
23389 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23390 });
23391 cx.update_editor(|_, _, _| {
23392 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
23393 });
23394
23395 cx.set_state(
23396 &r#"fn one() {
23397 let mut a = ˇtwo();
23398 }
23399
23400 fn two() {}"#
23401 .unindent(),
23402 );
23403 let navigated = cx
23404 .update_editor(|editor, window, cx| {
23405 editor.find_all_references(&FindAllReferences::default(), window, cx)
23406 })
23407 .unwrap()
23408 .await
23409 .expect("Failed to navigate to references");
23410 assert_eq!(
23411 navigated,
23412 Navigated::Yes,
23413 "Should have navigated to references from the FindAllReferences response"
23414 );
23415 cx.assert_editor_state(
23416 &r#"fn one() {
23417 let mut a = ˇtwo();
23418 }
23419
23420 fn two() {}"#
23421 .unindent(),
23422 );
23423 let editors = cx.update_workspace(|workspace, _, cx| {
23424 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23425 });
23426 cx.update_editor(|_, _, _| {
23427 assert_eq!(
23428 editors.len(),
23429 2,
23430 "should have re-used the previous multibuffer"
23431 );
23432 });
23433
23434 cx.set_state(
23435 &r#"fn one() {
23436 let mut a = ˇtwo();
23437 }
23438 fn three() {}
23439 fn two() {}"#
23440 .unindent(),
23441 );
23442 cx.lsp
23443 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
23444 Ok(Some(vec![
23445 lsp::Location {
23446 uri: params.text_document_position.text_document.uri.clone(),
23447 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
23448 },
23449 lsp::Location {
23450 uri: params.text_document_position.text_document.uri,
23451 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
23452 },
23453 ]))
23454 });
23455 let navigated = cx
23456 .update_editor(|editor, window, cx| {
23457 editor.find_all_references(&FindAllReferences::default(), window, cx)
23458 })
23459 .unwrap()
23460 .await
23461 .expect("Failed to navigate to references");
23462 assert_eq!(
23463 navigated,
23464 Navigated::Yes,
23465 "Should have navigated to references from the FindAllReferences response"
23466 );
23467 cx.assert_editor_state(
23468 &r#"fn one() {
23469 let mut a = ˇtwo();
23470 }
23471 fn three() {}
23472 fn two() {}"#
23473 .unindent(),
23474 );
23475 let editors = cx.update_workspace(|workspace, _, cx| {
23476 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
23477 });
23478 cx.update_editor(|_, _, _| {
23479 assert_eq!(
23480 editors.len(),
23481 3,
23482 "should have used a new multibuffer as offsets changed"
23483 );
23484 });
23485}
23486#[gpui::test]
23487async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
23488 init_test(cx, |_| {});
23489
23490 let language = Arc::new(Language::new(
23491 LanguageConfig::default(),
23492 Some(tree_sitter_rust::LANGUAGE.into()),
23493 ));
23494
23495 let text = r#"
23496 #[cfg(test)]
23497 mod tests() {
23498 #[test]
23499 fn runnable_1() {
23500 let a = 1;
23501 }
23502
23503 #[test]
23504 fn runnable_2() {
23505 let a = 1;
23506 let b = 2;
23507 }
23508 }
23509 "#
23510 .unindent();
23511
23512 let fs = FakeFs::new(cx.executor());
23513 fs.insert_file("/file.rs", Default::default()).await;
23514
23515 let project = Project::test(fs, ["/a".as_ref()], cx).await;
23516 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23517 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23518 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
23519 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
23520
23521 let editor = cx.new_window_entity(|window, cx| {
23522 Editor::new(
23523 EditorMode::full(),
23524 multi_buffer,
23525 Some(project.clone()),
23526 window,
23527 cx,
23528 )
23529 });
23530
23531 editor.update_in(cx, |editor, window, cx| {
23532 let snapshot = editor.buffer().read(cx).snapshot(cx);
23533 editor.tasks.insert(
23534 (buffer.read(cx).remote_id(), 3),
23535 RunnableTasks {
23536 templates: vec![],
23537 offset: snapshot.anchor_before(MultiBufferOffset(43)),
23538 column: 0,
23539 extra_variables: HashMap::default(),
23540 context_range: BufferOffset(43)..BufferOffset(85),
23541 },
23542 );
23543 editor.tasks.insert(
23544 (buffer.read(cx).remote_id(), 8),
23545 RunnableTasks {
23546 templates: vec![],
23547 offset: snapshot.anchor_before(MultiBufferOffset(86)),
23548 column: 0,
23549 extra_variables: HashMap::default(),
23550 context_range: BufferOffset(86)..BufferOffset(191),
23551 },
23552 );
23553
23554 // Test finding task when cursor is inside function body
23555 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23556 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
23557 });
23558 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23559 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
23560
23561 // Test finding task when cursor is on function name
23562 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23563 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
23564 });
23565 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
23566 assert_eq!(row, 8, "Should find task when cursor is on function name");
23567 });
23568}
23569
23570#[gpui::test]
23571async fn test_folding_buffers(cx: &mut TestAppContext) {
23572 init_test(cx, |_| {});
23573
23574 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23575 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
23576 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
23577
23578 let fs = FakeFs::new(cx.executor());
23579 fs.insert_tree(
23580 path!("/a"),
23581 json!({
23582 "first.rs": sample_text_1,
23583 "second.rs": sample_text_2,
23584 "third.rs": sample_text_3,
23585 }),
23586 )
23587 .await;
23588 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23589 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23590 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23591 let worktree = project.update(cx, |project, cx| {
23592 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23593 assert_eq!(worktrees.len(), 1);
23594 worktrees.pop().unwrap()
23595 });
23596 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23597
23598 let buffer_1 = project
23599 .update(cx, |project, cx| {
23600 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23601 })
23602 .await
23603 .unwrap();
23604 let buffer_2 = project
23605 .update(cx, |project, cx| {
23606 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23607 })
23608 .await
23609 .unwrap();
23610 let buffer_3 = project
23611 .update(cx, |project, cx| {
23612 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23613 })
23614 .await
23615 .unwrap();
23616
23617 let multi_buffer = cx.new(|cx| {
23618 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23619 multi_buffer.push_excerpts(
23620 buffer_1.clone(),
23621 [
23622 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23623 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23624 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23625 ],
23626 cx,
23627 );
23628 multi_buffer.push_excerpts(
23629 buffer_2.clone(),
23630 [
23631 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23632 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23633 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23634 ],
23635 cx,
23636 );
23637 multi_buffer.push_excerpts(
23638 buffer_3.clone(),
23639 [
23640 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
23641 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
23642 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
23643 ],
23644 cx,
23645 );
23646 multi_buffer
23647 });
23648 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23649 Editor::new(
23650 EditorMode::full(),
23651 multi_buffer.clone(),
23652 Some(project.clone()),
23653 window,
23654 cx,
23655 )
23656 });
23657
23658 assert_eq!(
23659 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23660 "\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",
23661 );
23662
23663 multi_buffer_editor.update(cx, |editor, cx| {
23664 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
23665 });
23666 assert_eq!(
23667 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23668 "\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",
23669 "After folding the first buffer, its text should not be displayed"
23670 );
23671
23672 multi_buffer_editor.update(cx, |editor, cx| {
23673 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
23674 });
23675 assert_eq!(
23676 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23677 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
23678 "After folding the second buffer, its text should not be displayed"
23679 );
23680
23681 multi_buffer_editor.update(cx, |editor, cx| {
23682 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23683 });
23684 assert_eq!(
23685 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23686 "\n\n\n\n\n",
23687 "After folding the third buffer, its text should not be displayed"
23688 );
23689
23690 // Emulate selection inside the fold logic, that should work
23691 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23692 editor
23693 .snapshot(window, cx)
23694 .next_line_boundary(Point::new(0, 4));
23695 });
23696
23697 multi_buffer_editor.update(cx, |editor, cx| {
23698 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23699 });
23700 assert_eq!(
23701 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23702 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
23703 "After unfolding the second buffer, its text should be displayed"
23704 );
23705
23706 // Typing inside of buffer 1 causes that buffer to be unfolded.
23707 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23708 assert_eq!(
23709 multi_buffer
23710 .read(cx)
23711 .snapshot(cx)
23712 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
23713 .collect::<String>(),
23714 "bbbb"
23715 );
23716 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23717 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
23718 });
23719 editor.handle_input("B", window, cx);
23720 });
23721
23722 assert_eq!(
23723 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23724 "\n\naaaa\nBbbbb\ncccc\n\n\nffff\ngggg\n\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
23725 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
23726 );
23727
23728 multi_buffer_editor.update(cx, |editor, cx| {
23729 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23730 });
23731 assert_eq!(
23732 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23733 "\n\naaaa\nBbbbb\ncccc\n\n\nffff\ngggg\n\n\njjjj\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
23734 "After unfolding the all buffers, all original text should be displayed"
23735 );
23736}
23737
23738#[gpui::test]
23739async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
23740 init_test(cx, |_| {});
23741
23742 let sample_text_1 = "1111\n2222\n3333".to_string();
23743 let sample_text_2 = "4444\n5555\n6666".to_string();
23744 let sample_text_3 = "7777\n8888\n9999".to_string();
23745
23746 let fs = FakeFs::new(cx.executor());
23747 fs.insert_tree(
23748 path!("/a"),
23749 json!({
23750 "first.rs": sample_text_1,
23751 "second.rs": sample_text_2,
23752 "third.rs": sample_text_3,
23753 }),
23754 )
23755 .await;
23756 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23757 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23758 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23759 let worktree = project.update(cx, |project, cx| {
23760 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23761 assert_eq!(worktrees.len(), 1);
23762 worktrees.pop().unwrap()
23763 });
23764 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23765
23766 let buffer_1 = project
23767 .update(cx, |project, cx| {
23768 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
23769 })
23770 .await
23771 .unwrap();
23772 let buffer_2 = project
23773 .update(cx, |project, cx| {
23774 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
23775 })
23776 .await
23777 .unwrap();
23778 let buffer_3 = project
23779 .update(cx, |project, cx| {
23780 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
23781 })
23782 .await
23783 .unwrap();
23784
23785 let multi_buffer = cx.new(|cx| {
23786 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23787 multi_buffer.push_excerpts(
23788 buffer_1.clone(),
23789 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23790 cx,
23791 );
23792 multi_buffer.push_excerpts(
23793 buffer_2.clone(),
23794 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23795 cx,
23796 );
23797 multi_buffer.push_excerpts(
23798 buffer_3.clone(),
23799 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
23800 cx,
23801 );
23802 multi_buffer
23803 });
23804
23805 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23806 Editor::new(
23807 EditorMode::full(),
23808 multi_buffer,
23809 Some(project.clone()),
23810 window,
23811 cx,
23812 )
23813 });
23814
23815 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
23816 assert_eq!(
23817 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23818 full_text,
23819 );
23820
23821 multi_buffer_editor.update(cx, |editor, cx| {
23822 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
23823 });
23824 assert_eq!(
23825 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23826 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
23827 "After folding the first buffer, its text should not be displayed"
23828 );
23829
23830 multi_buffer_editor.update(cx, |editor, cx| {
23831 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
23832 });
23833
23834 assert_eq!(
23835 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23836 "\n\n\n\n\n\n7777\n8888\n9999",
23837 "After folding the second buffer, its text should not be displayed"
23838 );
23839
23840 multi_buffer_editor.update(cx, |editor, cx| {
23841 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
23842 });
23843 assert_eq!(
23844 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23845 "\n\n\n\n\n",
23846 "After folding the third buffer, its text should not be displayed"
23847 );
23848
23849 multi_buffer_editor.update(cx, |editor, cx| {
23850 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
23851 });
23852 assert_eq!(
23853 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23854 "\n\n\n\n4444\n5555\n6666\n\n",
23855 "After unfolding the second buffer, its text should be displayed"
23856 );
23857
23858 multi_buffer_editor.update(cx, |editor, cx| {
23859 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
23860 });
23861 assert_eq!(
23862 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23863 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
23864 "After unfolding the first buffer, its text should be displayed"
23865 );
23866
23867 multi_buffer_editor.update(cx, |editor, cx| {
23868 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
23869 });
23870 assert_eq!(
23871 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23872 full_text,
23873 "After unfolding all buffers, all original text should be displayed"
23874 );
23875}
23876
23877#[gpui::test]
23878async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
23879 init_test(cx, |_| {});
23880
23881 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
23882
23883 let fs = FakeFs::new(cx.executor());
23884 fs.insert_tree(
23885 path!("/a"),
23886 json!({
23887 "main.rs": sample_text,
23888 }),
23889 )
23890 .await;
23891 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23892 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23893 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23894 let worktree = project.update(cx, |project, cx| {
23895 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
23896 assert_eq!(worktrees.len(), 1);
23897 worktrees.pop().unwrap()
23898 });
23899 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
23900
23901 let buffer_1 = project
23902 .update(cx, |project, cx| {
23903 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23904 })
23905 .await
23906 .unwrap();
23907
23908 let multi_buffer = cx.new(|cx| {
23909 let mut multi_buffer = MultiBuffer::new(ReadWrite);
23910 multi_buffer.push_excerpts(
23911 buffer_1.clone(),
23912 [ExcerptRange::new(
23913 Point::new(0, 0)
23914 ..Point::new(
23915 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
23916 0,
23917 ),
23918 )],
23919 cx,
23920 );
23921 multi_buffer
23922 });
23923 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
23924 Editor::new(
23925 EditorMode::full(),
23926 multi_buffer,
23927 Some(project.clone()),
23928 window,
23929 cx,
23930 )
23931 });
23932
23933 let selection_range = Point::new(1, 0)..Point::new(2, 0);
23934 multi_buffer_editor.update_in(cx, |editor, window, cx| {
23935 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
23936 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
23937 editor.highlight_text(
23938 HighlightKey::Editor,
23939 vec![highlight_range.clone()],
23940 HighlightStyle::color(Hsla::green()),
23941 cx,
23942 );
23943 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23944 s.select_ranges(Some(highlight_range))
23945 });
23946 });
23947
23948 let full_text = format!("\n\n{sample_text}");
23949 assert_eq!(
23950 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
23951 full_text,
23952 );
23953}
23954
23955#[gpui::test]
23956async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
23957 init_test(cx, |_| {});
23958 cx.update(|cx| {
23959 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
23960 "keymaps/default-linux.json",
23961 cx,
23962 )
23963 .unwrap();
23964 cx.bind_keys(default_key_bindings);
23965 });
23966
23967 let (editor, cx) = cx.add_window_view(|window, cx| {
23968 let multi_buffer = MultiBuffer::build_multi(
23969 [
23970 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
23971 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
23972 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
23973 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
23974 ],
23975 cx,
23976 );
23977 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
23978
23979 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
23980 // fold all but the second buffer, so that we test navigating between two
23981 // adjacent folded buffers, as well as folded buffers at the start and
23982 // end the multibuffer
23983 editor.fold_buffer(buffer_ids[0], cx);
23984 editor.fold_buffer(buffer_ids[2], cx);
23985 editor.fold_buffer(buffer_ids[3], cx);
23986
23987 editor
23988 });
23989 cx.simulate_resize(size(px(1000.), px(1000.)));
23990
23991 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
23992 cx.assert_excerpts_with_selections(indoc! {"
23993 [EXCERPT]
23994 ˇ[FOLDED]
23995 [EXCERPT]
23996 a1
23997 b1
23998 [EXCERPT]
23999 [FOLDED]
24000 [EXCERPT]
24001 [FOLDED]
24002 "
24003 });
24004 cx.simulate_keystroke("down");
24005 cx.assert_excerpts_with_selections(indoc! {"
24006 [EXCERPT]
24007 [FOLDED]
24008 [EXCERPT]
24009 ˇa1
24010 b1
24011 [EXCERPT]
24012 [FOLDED]
24013 [EXCERPT]
24014 [FOLDED]
24015 "
24016 });
24017 cx.simulate_keystroke("down");
24018 cx.assert_excerpts_with_selections(indoc! {"
24019 [EXCERPT]
24020 [FOLDED]
24021 [EXCERPT]
24022 a1
24023 ˇb1
24024 [EXCERPT]
24025 [FOLDED]
24026 [EXCERPT]
24027 [FOLDED]
24028 "
24029 });
24030 cx.simulate_keystroke("down");
24031 cx.assert_excerpts_with_selections(indoc! {"
24032 [EXCERPT]
24033 [FOLDED]
24034 [EXCERPT]
24035 a1
24036 b1
24037 ˇ[EXCERPT]
24038 [FOLDED]
24039 [EXCERPT]
24040 [FOLDED]
24041 "
24042 });
24043 cx.simulate_keystroke("down");
24044 cx.assert_excerpts_with_selections(indoc! {"
24045 [EXCERPT]
24046 [FOLDED]
24047 [EXCERPT]
24048 a1
24049 b1
24050 [EXCERPT]
24051 ˇ[FOLDED]
24052 [EXCERPT]
24053 [FOLDED]
24054 "
24055 });
24056 for _ in 0..5 {
24057 cx.simulate_keystroke("down");
24058 cx.assert_excerpts_with_selections(indoc! {"
24059 [EXCERPT]
24060 [FOLDED]
24061 [EXCERPT]
24062 a1
24063 b1
24064 [EXCERPT]
24065 [FOLDED]
24066 [EXCERPT]
24067 ˇ[FOLDED]
24068 "
24069 });
24070 }
24071
24072 cx.simulate_keystroke("up");
24073 cx.assert_excerpts_with_selections(indoc! {"
24074 [EXCERPT]
24075 [FOLDED]
24076 [EXCERPT]
24077 a1
24078 b1
24079 [EXCERPT]
24080 ˇ[FOLDED]
24081 [EXCERPT]
24082 [FOLDED]
24083 "
24084 });
24085 cx.simulate_keystroke("up");
24086 cx.assert_excerpts_with_selections(indoc! {"
24087 [EXCERPT]
24088 [FOLDED]
24089 [EXCERPT]
24090 a1
24091 b1
24092 ˇ[EXCERPT]
24093 [FOLDED]
24094 [EXCERPT]
24095 [FOLDED]
24096 "
24097 });
24098 cx.simulate_keystroke("up");
24099 cx.assert_excerpts_with_selections(indoc! {"
24100 [EXCERPT]
24101 [FOLDED]
24102 [EXCERPT]
24103 a1
24104 ˇb1
24105 [EXCERPT]
24106 [FOLDED]
24107 [EXCERPT]
24108 [FOLDED]
24109 "
24110 });
24111 cx.simulate_keystroke("up");
24112 cx.assert_excerpts_with_selections(indoc! {"
24113 [EXCERPT]
24114 [FOLDED]
24115 [EXCERPT]
24116 ˇa1
24117 b1
24118 [EXCERPT]
24119 [FOLDED]
24120 [EXCERPT]
24121 [FOLDED]
24122 "
24123 });
24124 for _ in 0..5 {
24125 cx.simulate_keystroke("up");
24126 cx.assert_excerpts_with_selections(indoc! {"
24127 [EXCERPT]
24128 ˇ[FOLDED]
24129 [EXCERPT]
24130 a1
24131 b1
24132 [EXCERPT]
24133 [FOLDED]
24134 [EXCERPT]
24135 [FOLDED]
24136 "
24137 });
24138 }
24139}
24140
24141#[gpui::test]
24142async fn test_edit_prediction_text(cx: &mut TestAppContext) {
24143 init_test(cx, |_| {});
24144
24145 // Simple insertion
24146 assert_highlighted_edits(
24147 "Hello, world!",
24148 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
24149 true,
24150 cx,
24151 |highlighted_edits, cx| {
24152 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
24153 assert_eq!(highlighted_edits.highlights.len(), 1);
24154 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
24155 assert_eq!(
24156 highlighted_edits.highlights[0].1.background_color,
24157 Some(cx.theme().status().created_background)
24158 );
24159 },
24160 )
24161 .await;
24162
24163 // Replacement
24164 assert_highlighted_edits(
24165 "This is a test.",
24166 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
24167 false,
24168 cx,
24169 |highlighted_edits, cx| {
24170 assert_eq!(highlighted_edits.text, "That is a test.");
24171 assert_eq!(highlighted_edits.highlights.len(), 1);
24172 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
24173 assert_eq!(
24174 highlighted_edits.highlights[0].1.background_color,
24175 Some(cx.theme().status().created_background)
24176 );
24177 },
24178 )
24179 .await;
24180
24181 // Multiple edits
24182 assert_highlighted_edits(
24183 "Hello, world!",
24184 vec![
24185 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
24186 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
24187 ],
24188 false,
24189 cx,
24190 |highlighted_edits, cx| {
24191 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
24192 assert_eq!(highlighted_edits.highlights.len(), 2);
24193 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
24194 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
24195 assert_eq!(
24196 highlighted_edits.highlights[0].1.background_color,
24197 Some(cx.theme().status().created_background)
24198 );
24199 assert_eq!(
24200 highlighted_edits.highlights[1].1.background_color,
24201 Some(cx.theme().status().created_background)
24202 );
24203 },
24204 )
24205 .await;
24206
24207 // Multiple lines with edits
24208 assert_highlighted_edits(
24209 "First line\nSecond line\nThird line\nFourth line",
24210 vec![
24211 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
24212 (
24213 Point::new(2, 0)..Point::new(2, 10),
24214 "New third line".to_string(),
24215 ),
24216 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
24217 ],
24218 false,
24219 cx,
24220 |highlighted_edits, cx| {
24221 assert_eq!(
24222 highlighted_edits.text,
24223 "Second modified\nNew third line\nFourth updated line"
24224 );
24225 assert_eq!(highlighted_edits.highlights.len(), 3);
24226 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
24227 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
24228 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
24229 for highlight in &highlighted_edits.highlights {
24230 assert_eq!(
24231 highlight.1.background_color,
24232 Some(cx.theme().status().created_background)
24233 );
24234 }
24235 },
24236 )
24237 .await;
24238}
24239
24240#[gpui::test]
24241async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
24242 init_test(cx, |_| {});
24243
24244 // Deletion
24245 assert_highlighted_edits(
24246 "Hello, world!",
24247 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
24248 true,
24249 cx,
24250 |highlighted_edits, cx| {
24251 assert_eq!(highlighted_edits.text, "Hello, world!");
24252 assert_eq!(highlighted_edits.highlights.len(), 1);
24253 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
24254 assert_eq!(
24255 highlighted_edits.highlights[0].1.background_color,
24256 Some(cx.theme().status().deleted_background)
24257 );
24258 },
24259 )
24260 .await;
24261
24262 // Insertion
24263 assert_highlighted_edits(
24264 "Hello, world!",
24265 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
24266 true,
24267 cx,
24268 |highlighted_edits, cx| {
24269 assert_eq!(highlighted_edits.highlights.len(), 1);
24270 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
24271 assert_eq!(
24272 highlighted_edits.highlights[0].1.background_color,
24273 Some(cx.theme().status().created_background)
24274 );
24275 },
24276 )
24277 .await;
24278}
24279
24280async fn assert_highlighted_edits(
24281 text: &str,
24282 edits: Vec<(Range<Point>, String)>,
24283 include_deletions: bool,
24284 cx: &mut TestAppContext,
24285 assertion_fn: impl Fn(HighlightedText, &App),
24286) {
24287 let window = cx.add_window(|window, cx| {
24288 let buffer = MultiBuffer::build_simple(text, cx);
24289 Editor::new(EditorMode::full(), buffer, None, window, cx)
24290 });
24291 let cx = &mut VisualTestContext::from_window(*window, cx);
24292
24293 let (buffer, snapshot) = window
24294 .update(cx, |editor, _window, cx| {
24295 (
24296 editor.buffer().clone(),
24297 editor.buffer().read(cx).snapshot(cx),
24298 )
24299 })
24300 .unwrap();
24301
24302 let edits = edits
24303 .into_iter()
24304 .map(|(range, edit)| {
24305 (
24306 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
24307 edit,
24308 )
24309 })
24310 .collect::<Vec<_>>();
24311
24312 let text_anchor_edits = edits
24313 .clone()
24314 .into_iter()
24315 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
24316 .collect::<Vec<_>>();
24317
24318 let edit_preview = window
24319 .update(cx, |_, _window, cx| {
24320 buffer
24321 .read(cx)
24322 .as_singleton()
24323 .unwrap()
24324 .read(cx)
24325 .preview_edits(text_anchor_edits.into(), cx)
24326 })
24327 .unwrap()
24328 .await;
24329
24330 cx.update(|_window, cx| {
24331 let highlighted_edits = edit_prediction_edit_text(
24332 snapshot.as_singleton().unwrap().2,
24333 &edits,
24334 &edit_preview,
24335 include_deletions,
24336 cx,
24337 );
24338 assertion_fn(highlighted_edits, cx)
24339 });
24340}
24341
24342#[track_caller]
24343fn assert_breakpoint(
24344 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
24345 path: &Arc<Path>,
24346 expected: Vec<(u32, Breakpoint)>,
24347) {
24348 if expected.is_empty() {
24349 assert!(!breakpoints.contains_key(path), "{}", path.display());
24350 } else {
24351 let mut breakpoint = breakpoints
24352 .get(path)
24353 .unwrap()
24354 .iter()
24355 .map(|breakpoint| {
24356 (
24357 breakpoint.row,
24358 Breakpoint {
24359 message: breakpoint.message.clone(),
24360 state: breakpoint.state,
24361 condition: breakpoint.condition.clone(),
24362 hit_condition: breakpoint.hit_condition.clone(),
24363 },
24364 )
24365 })
24366 .collect::<Vec<_>>();
24367
24368 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
24369
24370 assert_eq!(expected, breakpoint);
24371 }
24372}
24373
24374fn add_log_breakpoint_at_cursor(
24375 editor: &mut Editor,
24376 log_message: &str,
24377 window: &mut Window,
24378 cx: &mut Context<Editor>,
24379) {
24380 let (anchor, bp) = editor
24381 .breakpoints_at_cursors(window, cx)
24382 .first()
24383 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
24384 .unwrap_or_else(|| {
24385 let snapshot = editor.snapshot(window, cx);
24386 let cursor_position: Point =
24387 editor.selections.newest(&snapshot.display_snapshot).head();
24388
24389 let breakpoint_position = snapshot
24390 .buffer_snapshot()
24391 .anchor_before(Point::new(cursor_position.row, 0));
24392
24393 (breakpoint_position, Breakpoint::new_log(log_message))
24394 });
24395
24396 editor.edit_breakpoint_at_anchor(
24397 anchor,
24398 bp,
24399 BreakpointEditAction::EditLogMessage(log_message.into()),
24400 cx,
24401 );
24402}
24403
24404#[gpui::test]
24405async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
24406 init_test(cx, |_| {});
24407
24408 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24409 let fs = FakeFs::new(cx.executor());
24410 fs.insert_tree(
24411 path!("/a"),
24412 json!({
24413 "main.rs": sample_text,
24414 }),
24415 )
24416 .await;
24417 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24418 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24419 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24420
24421 let fs = FakeFs::new(cx.executor());
24422 fs.insert_tree(
24423 path!("/a"),
24424 json!({
24425 "main.rs": sample_text,
24426 }),
24427 )
24428 .await;
24429 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24430 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24431 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24432 let worktree_id = workspace
24433 .update(cx, |workspace, _window, cx| {
24434 workspace.project().update(cx, |project, cx| {
24435 project.worktrees(cx).next().unwrap().read(cx).id()
24436 })
24437 })
24438 .unwrap();
24439
24440 let buffer = project
24441 .update(cx, |project, cx| {
24442 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24443 })
24444 .await
24445 .unwrap();
24446
24447 let (editor, cx) = cx.add_window_view(|window, cx| {
24448 Editor::new(
24449 EditorMode::full(),
24450 MultiBuffer::build_from_buffer(buffer, cx),
24451 Some(project.clone()),
24452 window,
24453 cx,
24454 )
24455 });
24456
24457 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24458 let abs_path = project.read_with(cx, |project, cx| {
24459 project
24460 .absolute_path(&project_path, cx)
24461 .map(Arc::from)
24462 .unwrap()
24463 });
24464
24465 // assert we can add breakpoint on the first line
24466 editor.update_in(cx, |editor, window, cx| {
24467 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24468 editor.move_to_end(&MoveToEnd, window, cx);
24469 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24470 });
24471
24472 let breakpoints = editor.update(cx, |editor, cx| {
24473 editor
24474 .breakpoint_store()
24475 .as_ref()
24476 .unwrap()
24477 .read(cx)
24478 .all_source_breakpoints(cx)
24479 });
24480
24481 assert_eq!(1, breakpoints.len());
24482 assert_breakpoint(
24483 &breakpoints,
24484 &abs_path,
24485 vec![
24486 (0, Breakpoint::new_standard()),
24487 (3, Breakpoint::new_standard()),
24488 ],
24489 );
24490
24491 editor.update_in(cx, |editor, window, cx| {
24492 editor.move_to_beginning(&MoveToBeginning, window, cx);
24493 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24494 });
24495
24496 let breakpoints = editor.update(cx, |editor, cx| {
24497 editor
24498 .breakpoint_store()
24499 .as_ref()
24500 .unwrap()
24501 .read(cx)
24502 .all_source_breakpoints(cx)
24503 });
24504
24505 assert_eq!(1, breakpoints.len());
24506 assert_breakpoint(
24507 &breakpoints,
24508 &abs_path,
24509 vec![(3, Breakpoint::new_standard())],
24510 );
24511
24512 editor.update_in(cx, |editor, window, cx| {
24513 editor.move_to_end(&MoveToEnd, window, cx);
24514 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24515 });
24516
24517 let breakpoints = editor.update(cx, |editor, cx| {
24518 editor
24519 .breakpoint_store()
24520 .as_ref()
24521 .unwrap()
24522 .read(cx)
24523 .all_source_breakpoints(cx)
24524 });
24525
24526 assert_eq!(0, breakpoints.len());
24527 assert_breakpoint(&breakpoints, &abs_path, vec![]);
24528}
24529
24530#[gpui::test]
24531async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
24532 init_test(cx, |_| {});
24533
24534 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24535
24536 let fs = FakeFs::new(cx.executor());
24537 fs.insert_tree(
24538 path!("/a"),
24539 json!({
24540 "main.rs": sample_text,
24541 }),
24542 )
24543 .await;
24544 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24545 let (workspace, cx) =
24546 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24547
24548 let worktree_id = workspace.update(cx, |workspace, cx| {
24549 workspace.project().update(cx, |project, cx| {
24550 project.worktrees(cx).next().unwrap().read(cx).id()
24551 })
24552 });
24553
24554 let buffer = project
24555 .update(cx, |project, cx| {
24556 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24557 })
24558 .await
24559 .unwrap();
24560
24561 let (editor, cx) = cx.add_window_view(|window, cx| {
24562 Editor::new(
24563 EditorMode::full(),
24564 MultiBuffer::build_from_buffer(buffer, cx),
24565 Some(project.clone()),
24566 window,
24567 cx,
24568 )
24569 });
24570
24571 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24572 let abs_path = project.read_with(cx, |project, cx| {
24573 project
24574 .absolute_path(&project_path, cx)
24575 .map(Arc::from)
24576 .unwrap()
24577 });
24578
24579 editor.update_in(cx, |editor, window, cx| {
24580 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24581 });
24582
24583 let breakpoints = editor.update(cx, |editor, cx| {
24584 editor
24585 .breakpoint_store()
24586 .as_ref()
24587 .unwrap()
24588 .read(cx)
24589 .all_source_breakpoints(cx)
24590 });
24591
24592 assert_breakpoint(
24593 &breakpoints,
24594 &abs_path,
24595 vec![(0, Breakpoint::new_log("hello world"))],
24596 );
24597
24598 // Removing a log message from a log breakpoint should remove it
24599 editor.update_in(cx, |editor, window, cx| {
24600 add_log_breakpoint_at_cursor(editor, "", window, cx);
24601 });
24602
24603 let breakpoints = editor.update(cx, |editor, cx| {
24604 editor
24605 .breakpoint_store()
24606 .as_ref()
24607 .unwrap()
24608 .read(cx)
24609 .all_source_breakpoints(cx)
24610 });
24611
24612 assert_breakpoint(&breakpoints, &abs_path, vec![]);
24613
24614 editor.update_in(cx, |editor, window, cx| {
24615 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24616 editor.move_to_end(&MoveToEnd, window, cx);
24617 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24618 // Not adding a log message to a standard breakpoint shouldn't remove it
24619 add_log_breakpoint_at_cursor(editor, "", window, cx);
24620 });
24621
24622 let breakpoints = editor.update(cx, |editor, cx| {
24623 editor
24624 .breakpoint_store()
24625 .as_ref()
24626 .unwrap()
24627 .read(cx)
24628 .all_source_breakpoints(cx)
24629 });
24630
24631 assert_breakpoint(
24632 &breakpoints,
24633 &abs_path,
24634 vec![
24635 (0, Breakpoint::new_standard()),
24636 (3, Breakpoint::new_standard()),
24637 ],
24638 );
24639
24640 editor.update_in(cx, |editor, window, cx| {
24641 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
24642 });
24643
24644 let breakpoints = editor.update(cx, |editor, cx| {
24645 editor
24646 .breakpoint_store()
24647 .as_ref()
24648 .unwrap()
24649 .read(cx)
24650 .all_source_breakpoints(cx)
24651 });
24652
24653 assert_breakpoint(
24654 &breakpoints,
24655 &abs_path,
24656 vec![
24657 (0, Breakpoint::new_standard()),
24658 (3, Breakpoint::new_log("hello world")),
24659 ],
24660 );
24661
24662 editor.update_in(cx, |editor, window, cx| {
24663 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
24664 });
24665
24666 let breakpoints = editor.update(cx, |editor, cx| {
24667 editor
24668 .breakpoint_store()
24669 .as_ref()
24670 .unwrap()
24671 .read(cx)
24672 .all_source_breakpoints(cx)
24673 });
24674
24675 assert_breakpoint(
24676 &breakpoints,
24677 &abs_path,
24678 vec![
24679 (0, Breakpoint::new_standard()),
24680 (3, Breakpoint::new_log("hello Earth!!")),
24681 ],
24682 );
24683}
24684
24685/// This also tests that Editor::breakpoint_at_cursor_head is working properly
24686/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
24687/// or when breakpoints were placed out of order. This tests for a regression too
24688#[gpui::test]
24689async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
24690 init_test(cx, |_| {});
24691
24692 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24693 let fs = FakeFs::new(cx.executor());
24694 fs.insert_tree(
24695 path!("/a"),
24696 json!({
24697 "main.rs": sample_text,
24698 }),
24699 )
24700 .await;
24701 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24702 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24703 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24704
24705 let fs = FakeFs::new(cx.executor());
24706 fs.insert_tree(
24707 path!("/a"),
24708 json!({
24709 "main.rs": sample_text,
24710 }),
24711 )
24712 .await;
24713 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24714 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24715 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24716 let worktree_id = workspace
24717 .update(cx, |workspace, _window, cx| {
24718 workspace.project().update(cx, |project, cx| {
24719 project.worktrees(cx).next().unwrap().read(cx).id()
24720 })
24721 })
24722 .unwrap();
24723
24724 let buffer = project
24725 .update(cx, |project, cx| {
24726 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24727 })
24728 .await
24729 .unwrap();
24730
24731 let (editor, cx) = cx.add_window_view(|window, cx| {
24732 Editor::new(
24733 EditorMode::full(),
24734 MultiBuffer::build_from_buffer(buffer, cx),
24735 Some(project.clone()),
24736 window,
24737 cx,
24738 )
24739 });
24740
24741 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
24742 let abs_path = project.read_with(cx, |project, cx| {
24743 project
24744 .absolute_path(&project_path, cx)
24745 .map(Arc::from)
24746 .unwrap()
24747 });
24748
24749 // assert we can add breakpoint on the first line
24750 editor.update_in(cx, |editor, window, cx| {
24751 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24752 editor.move_to_end(&MoveToEnd, window, cx);
24753 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24754 editor.move_up(&MoveUp, window, cx);
24755 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24756 });
24757
24758 let breakpoints = editor.update(cx, |editor, cx| {
24759 editor
24760 .breakpoint_store()
24761 .as_ref()
24762 .unwrap()
24763 .read(cx)
24764 .all_source_breakpoints(cx)
24765 });
24766
24767 assert_eq!(1, breakpoints.len());
24768 assert_breakpoint(
24769 &breakpoints,
24770 &abs_path,
24771 vec![
24772 (0, Breakpoint::new_standard()),
24773 (2, Breakpoint::new_standard()),
24774 (3, Breakpoint::new_standard()),
24775 ],
24776 );
24777
24778 editor.update_in(cx, |editor, window, cx| {
24779 editor.move_to_beginning(&MoveToBeginning, window, cx);
24780 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24781 editor.move_to_end(&MoveToEnd, window, cx);
24782 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24783 // Disabling a breakpoint that doesn't exist should do nothing
24784 editor.move_up(&MoveUp, window, cx);
24785 editor.move_up(&MoveUp, window, cx);
24786 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24787 });
24788
24789 let breakpoints = editor.update(cx, |editor, cx| {
24790 editor
24791 .breakpoint_store()
24792 .as_ref()
24793 .unwrap()
24794 .read(cx)
24795 .all_source_breakpoints(cx)
24796 });
24797
24798 let disable_breakpoint = {
24799 let mut bp = Breakpoint::new_standard();
24800 bp.state = BreakpointState::Disabled;
24801 bp
24802 };
24803
24804 assert_eq!(1, breakpoints.len());
24805 assert_breakpoint(
24806 &breakpoints,
24807 &abs_path,
24808 vec![
24809 (0, disable_breakpoint.clone()),
24810 (2, Breakpoint::new_standard()),
24811 (3, disable_breakpoint.clone()),
24812 ],
24813 );
24814
24815 editor.update_in(cx, |editor, window, cx| {
24816 editor.move_to_beginning(&MoveToBeginning, window, cx);
24817 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24818 editor.move_to_end(&MoveToEnd, window, cx);
24819 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
24820 editor.move_up(&MoveUp, window, cx);
24821 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
24822 });
24823
24824 let breakpoints = editor.update(cx, |editor, cx| {
24825 editor
24826 .breakpoint_store()
24827 .as_ref()
24828 .unwrap()
24829 .read(cx)
24830 .all_source_breakpoints(cx)
24831 });
24832
24833 assert_eq!(1, breakpoints.len());
24834 assert_breakpoint(
24835 &breakpoints,
24836 &abs_path,
24837 vec![
24838 (0, Breakpoint::new_standard()),
24839 (2, disable_breakpoint),
24840 (3, Breakpoint::new_standard()),
24841 ],
24842 );
24843}
24844
24845#[gpui::test]
24846async fn test_breakpoint_phantom_indicator_collision_on_toggle(cx: &mut TestAppContext) {
24847 init_test(cx, |_| {});
24848
24849 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
24850 let fs = FakeFs::new(cx.executor());
24851 fs.insert_tree(
24852 path!("/a"),
24853 json!({
24854 "main.rs": sample_text,
24855 }),
24856 )
24857 .await;
24858 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24859 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24860 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24861 let worktree_id = workspace
24862 .update(cx, |workspace, _window, cx| {
24863 workspace.project().update(cx, |project, cx| {
24864 project.worktrees(cx).next().unwrap().read(cx).id()
24865 })
24866 })
24867 .unwrap();
24868
24869 let buffer = project
24870 .update(cx, |project, cx| {
24871 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
24872 })
24873 .await
24874 .unwrap();
24875
24876 let (editor, cx) = cx.add_window_view(|window, cx| {
24877 Editor::new(
24878 EditorMode::full(),
24879 MultiBuffer::build_from_buffer(buffer, cx),
24880 Some(project.clone()),
24881 window,
24882 cx,
24883 )
24884 });
24885
24886 // Simulate hovering over row 0 with no existing breakpoint.
24887 editor.update(cx, |editor, _cx| {
24888 editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {
24889 display_row: DisplayRow(0),
24890 is_active: true,
24891 collides_with_existing_breakpoint: false,
24892 });
24893 });
24894
24895 // Toggle breakpoint on the same row (row 0) — collision should flip to true.
24896 editor.update_in(cx, |editor, window, cx| {
24897 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24898 });
24899 editor.update(cx, |editor, _cx| {
24900 let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
24901 assert!(
24902 indicator.collides_with_existing_breakpoint,
24903 "Adding a breakpoint on the hovered row should set collision to true"
24904 );
24905 });
24906
24907 // Toggle again on the same row — breakpoint is removed, collision should flip back to false.
24908 editor.update_in(cx, |editor, window, cx| {
24909 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24910 });
24911 editor.update(cx, |editor, _cx| {
24912 let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
24913 assert!(
24914 !indicator.collides_with_existing_breakpoint,
24915 "Removing a breakpoint on the hovered row should set collision to false"
24916 );
24917 });
24918
24919 // Now move cursor to row 2 while phantom indicator stays on row 0.
24920 editor.update_in(cx, |editor, window, cx| {
24921 editor.move_down(&MoveDown, window, cx);
24922 editor.move_down(&MoveDown, window, cx);
24923 });
24924
24925 // Ensure phantom indicator is still on row 0, not colliding.
24926 editor.update(cx, |editor, _cx| {
24927 editor.gutter_breakpoint_indicator.0 = Some(PhantomBreakpointIndicator {
24928 display_row: DisplayRow(0),
24929 is_active: true,
24930 collides_with_existing_breakpoint: false,
24931 });
24932 });
24933
24934 // Toggle breakpoint on row 2 (cursor row) — phantom on row 0 should NOT be affected.
24935 editor.update_in(cx, |editor, window, cx| {
24936 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
24937 });
24938 editor.update(cx, |editor, _cx| {
24939 let indicator = editor.gutter_breakpoint_indicator.0.unwrap();
24940 assert!(
24941 !indicator.collides_with_existing_breakpoint,
24942 "Toggling a breakpoint on a different row should not affect the phantom indicator"
24943 );
24944 });
24945}
24946
24947#[gpui::test]
24948async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
24949 init_test(cx, |_| {});
24950 let capabilities = lsp::ServerCapabilities {
24951 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
24952 prepare_provider: Some(true),
24953 work_done_progress_options: Default::default(),
24954 })),
24955 ..Default::default()
24956 };
24957 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
24958
24959 cx.set_state(indoc! {"
24960 struct Fˇoo {}
24961 "});
24962
24963 cx.update_editor(|editor, _, cx| {
24964 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
24965 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
24966 editor.highlight_background(
24967 HighlightKey::DocumentHighlightRead,
24968 &[highlight_range],
24969 |_, theme| theme.colors().editor_document_highlight_read_background,
24970 cx,
24971 );
24972 });
24973
24974 let mut prepare_rename_handler = cx
24975 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
24976 move |_, _, _| async move {
24977 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
24978 start: lsp::Position {
24979 line: 0,
24980 character: 7,
24981 },
24982 end: lsp::Position {
24983 line: 0,
24984 character: 10,
24985 },
24986 })))
24987 },
24988 );
24989 let prepare_rename_task = cx
24990 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
24991 .expect("Prepare rename was not started");
24992 prepare_rename_handler.next().await.unwrap();
24993 prepare_rename_task.await.expect("Prepare rename failed");
24994
24995 let mut rename_handler =
24996 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
24997 let edit = lsp::TextEdit {
24998 range: lsp::Range {
24999 start: lsp::Position {
25000 line: 0,
25001 character: 7,
25002 },
25003 end: lsp::Position {
25004 line: 0,
25005 character: 10,
25006 },
25007 },
25008 new_text: "FooRenamed".to_string(),
25009 };
25010 Ok(Some(lsp::WorkspaceEdit::new(
25011 // Specify the same edit twice
25012 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
25013 )))
25014 });
25015 let rename_task = cx
25016 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
25017 .expect("Confirm rename was not started");
25018 rename_handler.next().await.unwrap();
25019 rename_task.await.expect("Confirm rename failed");
25020 cx.run_until_parked();
25021
25022 // Despite two edits, only one is actually applied as those are identical
25023 cx.assert_editor_state(indoc! {"
25024 struct FooRenamedˇ {}
25025 "});
25026}
25027
25028#[gpui::test]
25029async fn test_rename_without_prepare(cx: &mut TestAppContext) {
25030 init_test(cx, |_| {});
25031 // These capabilities indicate that the server does not support prepare rename.
25032 let capabilities = lsp::ServerCapabilities {
25033 rename_provider: Some(lsp::OneOf::Left(true)),
25034 ..Default::default()
25035 };
25036 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
25037
25038 cx.set_state(indoc! {"
25039 struct Fˇoo {}
25040 "});
25041
25042 cx.update_editor(|editor, _window, cx| {
25043 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
25044 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
25045 editor.highlight_background(
25046 HighlightKey::DocumentHighlightRead,
25047 &[highlight_range],
25048 |_, theme| theme.colors().editor_document_highlight_read_background,
25049 cx,
25050 );
25051 });
25052
25053 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
25054 .expect("Prepare rename was not started")
25055 .await
25056 .expect("Prepare rename failed");
25057
25058 let mut rename_handler =
25059 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
25060 let edit = lsp::TextEdit {
25061 range: lsp::Range {
25062 start: lsp::Position {
25063 line: 0,
25064 character: 7,
25065 },
25066 end: lsp::Position {
25067 line: 0,
25068 character: 10,
25069 },
25070 },
25071 new_text: "FooRenamed".to_string(),
25072 };
25073 Ok(Some(lsp::WorkspaceEdit::new(
25074 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
25075 )))
25076 });
25077 let rename_task = cx
25078 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
25079 .expect("Confirm rename was not started");
25080 rename_handler.next().await.unwrap();
25081 rename_task.await.expect("Confirm rename failed");
25082 cx.run_until_parked();
25083
25084 // Correct range is renamed, as `surrounding_word` is used to find it.
25085 cx.assert_editor_state(indoc! {"
25086 struct FooRenamedˇ {}
25087 "});
25088}
25089
25090#[gpui::test]
25091async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
25092 init_test(cx, |_| {});
25093 let mut cx = EditorTestContext::new(cx).await;
25094
25095 let language = Arc::new(
25096 Language::new(
25097 LanguageConfig::default(),
25098 Some(tree_sitter_html::LANGUAGE.into()),
25099 )
25100 .with_brackets_query(
25101 r#"
25102 ("<" @open "/>" @close)
25103 ("</" @open ">" @close)
25104 ("<" @open ">" @close)
25105 ("\"" @open "\"" @close)
25106 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
25107 "#,
25108 )
25109 .unwrap(),
25110 );
25111 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25112
25113 cx.set_state(indoc! {"
25114 <span>ˇ</span>
25115 "});
25116 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
25117 cx.assert_editor_state(indoc! {"
25118 <span>
25119 ˇ
25120 </span>
25121 "});
25122
25123 cx.set_state(indoc! {"
25124 <span><span></span>ˇ</span>
25125 "});
25126 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
25127 cx.assert_editor_state(indoc! {"
25128 <span><span></span>
25129 ˇ</span>
25130 "});
25131
25132 cx.set_state(indoc! {"
25133 <span>ˇ
25134 </span>
25135 "});
25136 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
25137 cx.assert_editor_state(indoc! {"
25138 <span>
25139 ˇ
25140 </span>
25141 "});
25142}
25143
25144#[gpui::test(iterations = 10)]
25145async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
25146 init_test(cx, |_| {});
25147
25148 let fs = FakeFs::new(cx.executor());
25149 fs.insert_tree(
25150 path!("/dir"),
25151 json!({
25152 "a.ts": "a",
25153 }),
25154 )
25155 .await;
25156
25157 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
25158 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25159 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
25160
25161 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25162 language_registry.add(Arc::new(Language::new(
25163 LanguageConfig {
25164 name: "TypeScript".into(),
25165 matcher: LanguageMatcher {
25166 path_suffixes: vec!["ts".to_string()],
25167 ..Default::default()
25168 },
25169 ..Default::default()
25170 },
25171 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
25172 )));
25173 let mut fake_language_servers = language_registry.register_fake_lsp(
25174 "TypeScript",
25175 FakeLspAdapter {
25176 capabilities: lsp::ServerCapabilities {
25177 code_lens_provider: Some(lsp::CodeLensOptions {
25178 resolve_provider: Some(true),
25179 }),
25180 execute_command_provider: Some(lsp::ExecuteCommandOptions {
25181 commands: vec!["_the/command".to_string()],
25182 ..lsp::ExecuteCommandOptions::default()
25183 }),
25184 ..lsp::ServerCapabilities::default()
25185 },
25186 ..FakeLspAdapter::default()
25187 },
25188 );
25189
25190 let editor = workspace
25191 .update(cx, |workspace, window, cx| {
25192 workspace.open_abs_path(
25193 PathBuf::from(path!("/dir/a.ts")),
25194 OpenOptions::default(),
25195 window,
25196 cx,
25197 )
25198 })
25199 .unwrap()
25200 .await
25201 .unwrap()
25202 .downcast::<Editor>()
25203 .unwrap();
25204 cx.executor().run_until_parked();
25205
25206 let fake_server = fake_language_servers.next().await.unwrap();
25207
25208 let buffer = editor.update(cx, |editor, cx| {
25209 editor
25210 .buffer()
25211 .read(cx)
25212 .as_singleton()
25213 .expect("have opened a single file by path")
25214 });
25215
25216 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
25217 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
25218 drop(buffer_snapshot);
25219 let actions = cx
25220 .update_window(*workspace, |_, window, cx| {
25221 project.code_actions(&buffer, anchor..anchor, window, cx)
25222 })
25223 .unwrap();
25224
25225 fake_server
25226 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
25227 Ok(Some(vec![
25228 lsp::CodeLens {
25229 range: lsp::Range::default(),
25230 command: Some(lsp::Command {
25231 title: "Code lens command".to_owned(),
25232 command: "_the/command".to_owned(),
25233 arguments: None,
25234 }),
25235 data: None,
25236 },
25237 lsp::CodeLens {
25238 range: lsp::Range::default(),
25239 command: Some(lsp::Command {
25240 title: "Command not in capabilities".to_owned(),
25241 command: "not in capabilities".to_owned(),
25242 arguments: None,
25243 }),
25244 data: None,
25245 },
25246 lsp::CodeLens {
25247 range: lsp::Range {
25248 start: lsp::Position {
25249 line: 1,
25250 character: 1,
25251 },
25252 end: lsp::Position {
25253 line: 1,
25254 character: 1,
25255 },
25256 },
25257 command: Some(lsp::Command {
25258 title: "Command not in range".to_owned(),
25259 command: "_the/command".to_owned(),
25260 arguments: None,
25261 }),
25262 data: None,
25263 },
25264 ]))
25265 })
25266 .next()
25267 .await;
25268
25269 let actions = actions.await.unwrap();
25270 assert_eq!(
25271 actions.len(),
25272 1,
25273 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
25274 );
25275 let action = actions[0].clone();
25276 let apply = project.update(cx, |project, cx| {
25277 project.apply_code_action(buffer.clone(), action, true, cx)
25278 });
25279
25280 // Resolving the code action does not populate its edits. In absence of
25281 // edits, we must execute the given command.
25282 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
25283 |mut lens, _| async move {
25284 let lens_command = lens.command.as_mut().expect("should have a command");
25285 assert_eq!(lens_command.title, "Code lens command");
25286 lens_command.arguments = Some(vec![json!("the-argument")]);
25287 Ok(lens)
25288 },
25289 );
25290
25291 // While executing the command, the language server sends the editor
25292 // a `workspaceEdit` request.
25293 fake_server
25294 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
25295 let fake = fake_server.clone();
25296 move |params, _| {
25297 assert_eq!(params.command, "_the/command");
25298 let fake = fake.clone();
25299 async move {
25300 fake.server
25301 .request::<lsp::request::ApplyWorkspaceEdit>(
25302 lsp::ApplyWorkspaceEditParams {
25303 label: None,
25304 edit: lsp::WorkspaceEdit {
25305 changes: Some(
25306 [(
25307 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
25308 vec![lsp::TextEdit {
25309 range: lsp::Range::new(
25310 lsp::Position::new(0, 0),
25311 lsp::Position::new(0, 0),
25312 ),
25313 new_text: "X".into(),
25314 }],
25315 )]
25316 .into_iter()
25317 .collect(),
25318 ),
25319 ..lsp::WorkspaceEdit::default()
25320 },
25321 },
25322 DEFAULT_LSP_REQUEST_TIMEOUT,
25323 )
25324 .await
25325 .into_response()
25326 .unwrap();
25327 Ok(Some(json!(null)))
25328 }
25329 }
25330 })
25331 .next()
25332 .await;
25333
25334 // Applying the code lens command returns a project transaction containing the edits
25335 // sent by the language server in its `workspaceEdit` request.
25336 let transaction = apply.await.unwrap();
25337 assert!(transaction.0.contains_key(&buffer));
25338 buffer.update(cx, |buffer, cx| {
25339 assert_eq!(buffer.text(), "Xa");
25340 buffer.undo(cx);
25341 assert_eq!(buffer.text(), "a");
25342 });
25343
25344 let actions_after_edits = cx
25345 .update_window(*workspace, |_, window, cx| {
25346 project.code_actions(&buffer, anchor..anchor, window, cx)
25347 })
25348 .unwrap()
25349 .await
25350 .unwrap();
25351 assert_eq!(
25352 actions, actions_after_edits,
25353 "For the same selection, same code lens actions should be returned"
25354 );
25355
25356 let _responses =
25357 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
25358 panic!("No more code lens requests are expected");
25359 });
25360 editor.update_in(cx, |editor, window, cx| {
25361 editor.select_all(&SelectAll, window, cx);
25362 });
25363 cx.executor().run_until_parked();
25364 let new_actions = cx
25365 .update_window(*workspace, |_, window, cx| {
25366 project.code_actions(&buffer, anchor..anchor, window, cx)
25367 })
25368 .unwrap()
25369 .await
25370 .unwrap();
25371 assert_eq!(
25372 actions, new_actions,
25373 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
25374 );
25375}
25376
25377#[gpui::test]
25378async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
25379 init_test(cx, |_| {});
25380
25381 let fs = FakeFs::new(cx.executor());
25382 let main_text = r#"fn main() {
25383println!("1");
25384println!("2");
25385println!("3");
25386println!("4");
25387println!("5");
25388}"#;
25389 let lib_text = "mod foo {}";
25390 fs.insert_tree(
25391 path!("/a"),
25392 json!({
25393 "lib.rs": lib_text,
25394 "main.rs": main_text,
25395 }),
25396 )
25397 .await;
25398
25399 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25400 let (workspace, cx) =
25401 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25402 let worktree_id = workspace.update(cx, |workspace, cx| {
25403 workspace.project().update(cx, |project, cx| {
25404 project.worktrees(cx).next().unwrap().read(cx).id()
25405 })
25406 });
25407
25408 let expected_ranges = vec![
25409 Point::new(0, 0)..Point::new(0, 0),
25410 Point::new(1, 0)..Point::new(1, 1),
25411 Point::new(2, 0)..Point::new(2, 2),
25412 Point::new(3, 0)..Point::new(3, 3),
25413 ];
25414
25415 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25416 let editor_1 = workspace
25417 .update_in(cx, |workspace, window, cx| {
25418 workspace.open_path(
25419 (worktree_id, rel_path("main.rs")),
25420 Some(pane_1.downgrade()),
25421 true,
25422 window,
25423 cx,
25424 )
25425 })
25426 .unwrap()
25427 .await
25428 .downcast::<Editor>()
25429 .unwrap();
25430 pane_1.update(cx, |pane, cx| {
25431 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25432 open_editor.update(cx, |editor, cx| {
25433 assert_eq!(
25434 editor.display_text(cx),
25435 main_text,
25436 "Original main.rs text on initial open",
25437 );
25438 assert_eq!(
25439 editor
25440 .selections
25441 .all::<Point>(&editor.display_snapshot(cx))
25442 .into_iter()
25443 .map(|s| s.range())
25444 .collect::<Vec<_>>(),
25445 vec![Point::zero()..Point::zero()],
25446 "Default selections on initial open",
25447 );
25448 })
25449 });
25450 editor_1.update_in(cx, |editor, window, cx| {
25451 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25452 s.select_ranges(expected_ranges.clone());
25453 });
25454 });
25455
25456 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
25457 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
25458 });
25459 let editor_2 = workspace
25460 .update_in(cx, |workspace, window, cx| {
25461 workspace.open_path(
25462 (worktree_id, rel_path("main.rs")),
25463 Some(pane_2.downgrade()),
25464 true,
25465 window,
25466 cx,
25467 )
25468 })
25469 .unwrap()
25470 .await
25471 .downcast::<Editor>()
25472 .unwrap();
25473 pane_2.update(cx, |pane, cx| {
25474 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25475 open_editor.update(cx, |editor, cx| {
25476 assert_eq!(
25477 editor.display_text(cx),
25478 main_text,
25479 "Original main.rs text on initial open in another panel",
25480 );
25481 assert_eq!(
25482 editor
25483 .selections
25484 .all::<Point>(&editor.display_snapshot(cx))
25485 .into_iter()
25486 .map(|s| s.range())
25487 .collect::<Vec<_>>(),
25488 vec![Point::zero()..Point::zero()],
25489 "Default selections on initial open in another panel",
25490 );
25491 })
25492 });
25493
25494 editor_2.update_in(cx, |editor, window, cx| {
25495 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
25496 });
25497
25498 let _other_editor_1 = workspace
25499 .update_in(cx, |workspace, window, cx| {
25500 workspace.open_path(
25501 (worktree_id, rel_path("lib.rs")),
25502 Some(pane_1.downgrade()),
25503 true,
25504 window,
25505 cx,
25506 )
25507 })
25508 .unwrap()
25509 .await
25510 .downcast::<Editor>()
25511 .unwrap();
25512 pane_1
25513 .update_in(cx, |pane, window, cx| {
25514 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
25515 })
25516 .await
25517 .unwrap();
25518 drop(editor_1);
25519 pane_1.update(cx, |pane, cx| {
25520 pane.active_item()
25521 .unwrap()
25522 .downcast::<Editor>()
25523 .unwrap()
25524 .update(cx, |editor, cx| {
25525 assert_eq!(
25526 editor.display_text(cx),
25527 lib_text,
25528 "Other file should be open and active",
25529 );
25530 });
25531 assert_eq!(pane.items().count(), 1, "No other editors should be open");
25532 });
25533
25534 let _other_editor_2 = workspace
25535 .update_in(cx, |workspace, window, cx| {
25536 workspace.open_path(
25537 (worktree_id, rel_path("lib.rs")),
25538 Some(pane_2.downgrade()),
25539 true,
25540 window,
25541 cx,
25542 )
25543 })
25544 .unwrap()
25545 .await
25546 .downcast::<Editor>()
25547 .unwrap();
25548 pane_2
25549 .update_in(cx, |pane, window, cx| {
25550 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
25551 })
25552 .await
25553 .unwrap();
25554 drop(editor_2);
25555 pane_2.update(cx, |pane, cx| {
25556 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25557 open_editor.update(cx, |editor, cx| {
25558 assert_eq!(
25559 editor.display_text(cx),
25560 lib_text,
25561 "Other file should be open and active in another panel too",
25562 );
25563 });
25564 assert_eq!(
25565 pane.items().count(),
25566 1,
25567 "No other editors should be open in another pane",
25568 );
25569 });
25570
25571 let _editor_1_reopened = workspace
25572 .update_in(cx, |workspace, window, cx| {
25573 workspace.open_path(
25574 (worktree_id, rel_path("main.rs")),
25575 Some(pane_1.downgrade()),
25576 true,
25577 window,
25578 cx,
25579 )
25580 })
25581 .unwrap()
25582 .await
25583 .downcast::<Editor>()
25584 .unwrap();
25585 let _editor_2_reopened = workspace
25586 .update_in(cx, |workspace, window, cx| {
25587 workspace.open_path(
25588 (worktree_id, rel_path("main.rs")),
25589 Some(pane_2.downgrade()),
25590 true,
25591 window,
25592 cx,
25593 )
25594 })
25595 .unwrap()
25596 .await
25597 .downcast::<Editor>()
25598 .unwrap();
25599 pane_1.update(cx, |pane, cx| {
25600 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25601 open_editor.update(cx, |editor, cx| {
25602 assert_eq!(
25603 editor.display_text(cx),
25604 main_text,
25605 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
25606 );
25607 assert_eq!(
25608 editor
25609 .selections
25610 .all::<Point>(&editor.display_snapshot(cx))
25611 .into_iter()
25612 .map(|s| s.range())
25613 .collect::<Vec<_>>(),
25614 expected_ranges,
25615 "Previous editor in the 1st panel had selections and should get them restored on reopen",
25616 );
25617 })
25618 });
25619 pane_2.update(cx, |pane, cx| {
25620 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25621 open_editor.update(cx, |editor, cx| {
25622 assert_eq!(
25623 editor.display_text(cx),
25624 r#"fn main() {
25625⋯rintln!("1");
25626⋯intln!("2");
25627⋯ntln!("3");
25628println!("4");
25629println!("5");
25630}"#,
25631 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
25632 );
25633 assert_eq!(
25634 editor
25635 .selections
25636 .all::<Point>(&editor.display_snapshot(cx))
25637 .into_iter()
25638 .map(|s| s.range())
25639 .collect::<Vec<_>>(),
25640 vec![Point::zero()..Point::zero()],
25641 "Previous editor in the 2nd pane had no selections changed hence should restore none",
25642 );
25643 })
25644 });
25645}
25646
25647#[gpui::test]
25648async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
25649 init_test(cx, |_| {});
25650
25651 let fs = FakeFs::new(cx.executor());
25652 let main_text = r#"fn main() {
25653println!("1");
25654println!("2");
25655println!("3");
25656println!("4");
25657println!("5");
25658}"#;
25659 let lib_text = "mod foo {}";
25660 fs.insert_tree(
25661 path!("/a"),
25662 json!({
25663 "lib.rs": lib_text,
25664 "main.rs": main_text,
25665 }),
25666 )
25667 .await;
25668
25669 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25670 let (workspace, cx) =
25671 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25672 let worktree_id = workspace.update(cx, |workspace, cx| {
25673 workspace.project().update(cx, |project, cx| {
25674 project.worktrees(cx).next().unwrap().read(cx).id()
25675 })
25676 });
25677
25678 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
25679 let editor = workspace
25680 .update_in(cx, |workspace, window, cx| {
25681 workspace.open_path(
25682 (worktree_id, rel_path("main.rs")),
25683 Some(pane.downgrade()),
25684 true,
25685 window,
25686 cx,
25687 )
25688 })
25689 .unwrap()
25690 .await
25691 .downcast::<Editor>()
25692 .unwrap();
25693 pane.update(cx, |pane, cx| {
25694 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25695 open_editor.update(cx, |editor, cx| {
25696 assert_eq!(
25697 editor.display_text(cx),
25698 main_text,
25699 "Original main.rs text on initial open",
25700 );
25701 })
25702 });
25703 editor.update_in(cx, |editor, window, cx| {
25704 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
25705 });
25706
25707 cx.update_global(|store: &mut SettingsStore, cx| {
25708 store.update_user_settings(cx, |s| {
25709 s.workspace.restore_on_file_reopen = Some(false);
25710 });
25711 });
25712 editor.update_in(cx, |editor, window, cx| {
25713 editor.fold_ranges(
25714 vec![
25715 Point::new(1, 0)..Point::new(1, 1),
25716 Point::new(2, 0)..Point::new(2, 2),
25717 Point::new(3, 0)..Point::new(3, 3),
25718 ],
25719 false,
25720 window,
25721 cx,
25722 );
25723 });
25724 pane.update_in(cx, |pane, window, cx| {
25725 pane.close_all_items(&CloseAllItems::default(), window, cx)
25726 })
25727 .await
25728 .unwrap();
25729 pane.update(cx, |pane, _| {
25730 assert!(pane.active_item().is_none());
25731 });
25732 cx.update_global(|store: &mut SettingsStore, cx| {
25733 store.update_user_settings(cx, |s| {
25734 s.workspace.restore_on_file_reopen = Some(true);
25735 });
25736 });
25737
25738 let _editor_reopened = workspace
25739 .update_in(cx, |workspace, window, cx| {
25740 workspace.open_path(
25741 (worktree_id, rel_path("main.rs")),
25742 Some(pane.downgrade()),
25743 true,
25744 window,
25745 cx,
25746 )
25747 })
25748 .unwrap()
25749 .await
25750 .downcast::<Editor>()
25751 .unwrap();
25752 pane.update(cx, |pane, cx| {
25753 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
25754 open_editor.update(cx, |editor, cx| {
25755 assert_eq!(
25756 editor.display_text(cx),
25757 main_text,
25758 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
25759 );
25760 })
25761 });
25762}
25763
25764#[gpui::test]
25765async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
25766 struct EmptyModalView {
25767 focus_handle: gpui::FocusHandle,
25768 }
25769 impl EventEmitter<DismissEvent> for EmptyModalView {}
25770 impl Render for EmptyModalView {
25771 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
25772 div()
25773 }
25774 }
25775 impl Focusable for EmptyModalView {
25776 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
25777 self.focus_handle.clone()
25778 }
25779 }
25780 impl workspace::ModalView for EmptyModalView {}
25781 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
25782 EmptyModalView {
25783 focus_handle: cx.focus_handle(),
25784 }
25785 }
25786
25787 init_test(cx, |_| {});
25788
25789 let fs = FakeFs::new(cx.executor());
25790 let project = Project::test(fs, [], cx).await;
25791 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25792 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
25793 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
25794 let editor = cx.new_window_entity(|window, cx| {
25795 Editor::new(
25796 EditorMode::full(),
25797 buffer,
25798 Some(project.clone()),
25799 window,
25800 cx,
25801 )
25802 });
25803 workspace
25804 .update(cx, |workspace, window, cx| {
25805 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
25806 })
25807 .unwrap();
25808 editor.update_in(cx, |editor, window, cx| {
25809 editor.open_context_menu(&OpenContextMenu, window, cx);
25810 assert!(editor.mouse_context_menu.is_some());
25811 });
25812 workspace
25813 .update(cx, |workspace, window, cx| {
25814 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
25815 })
25816 .unwrap();
25817 cx.read(|cx| {
25818 assert!(editor.read(cx).mouse_context_menu.is_none());
25819 });
25820}
25821
25822fn set_linked_edit_ranges(
25823 opening: (Point, Point),
25824 closing: (Point, Point),
25825 editor: &mut Editor,
25826 cx: &mut Context<Editor>,
25827) {
25828 let Some((buffer, _)) = editor
25829 .buffer
25830 .read(cx)
25831 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
25832 else {
25833 panic!("Failed to get buffer for selection position");
25834 };
25835 let buffer = buffer.read(cx);
25836 let buffer_id = buffer.remote_id();
25837 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
25838 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
25839 let mut linked_ranges = HashMap::default();
25840 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
25841 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
25842}
25843
25844#[gpui::test]
25845async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
25846 init_test(cx, |_| {});
25847
25848 let fs = FakeFs::new(cx.executor());
25849 fs.insert_file(path!("/file.html"), Default::default())
25850 .await;
25851
25852 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
25853
25854 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25855 let html_language = Arc::new(Language::new(
25856 LanguageConfig {
25857 name: "HTML".into(),
25858 matcher: LanguageMatcher {
25859 path_suffixes: vec!["html".to_string()],
25860 ..LanguageMatcher::default()
25861 },
25862 brackets: BracketPairConfig {
25863 pairs: vec![BracketPair {
25864 start: "<".into(),
25865 end: ">".into(),
25866 close: true,
25867 ..Default::default()
25868 }],
25869 ..Default::default()
25870 },
25871 ..Default::default()
25872 },
25873 Some(tree_sitter_html::LANGUAGE.into()),
25874 ));
25875 language_registry.add(html_language);
25876 let mut fake_servers = language_registry.register_fake_lsp(
25877 "HTML",
25878 FakeLspAdapter {
25879 capabilities: lsp::ServerCapabilities {
25880 completion_provider: Some(lsp::CompletionOptions {
25881 resolve_provider: Some(true),
25882 ..Default::default()
25883 }),
25884 ..Default::default()
25885 },
25886 ..Default::default()
25887 },
25888 );
25889
25890 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25891 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25892
25893 let worktree_id = workspace
25894 .update(cx, |workspace, _window, cx| {
25895 workspace.project().update(cx, |project, cx| {
25896 project.worktrees(cx).next().unwrap().read(cx).id()
25897 })
25898 })
25899 .unwrap();
25900 project
25901 .update(cx, |project, cx| {
25902 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
25903 })
25904 .await
25905 .unwrap();
25906 let editor = workspace
25907 .update(cx, |workspace, window, cx| {
25908 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
25909 })
25910 .unwrap()
25911 .await
25912 .unwrap()
25913 .downcast::<Editor>()
25914 .unwrap();
25915
25916 let fake_server = fake_servers.next().await.unwrap();
25917 cx.run_until_parked();
25918 editor.update_in(cx, |editor, window, cx| {
25919 editor.set_text("<ad></ad>", window, cx);
25920 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
25921 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
25922 });
25923 set_linked_edit_ranges(
25924 (Point::new(0, 1), Point::new(0, 3)),
25925 (Point::new(0, 6), Point::new(0, 8)),
25926 editor,
25927 cx,
25928 );
25929 });
25930 let mut completion_handle =
25931 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
25932 Ok(Some(lsp::CompletionResponse::Array(vec![
25933 lsp::CompletionItem {
25934 label: "head".to_string(),
25935 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25936 lsp::InsertReplaceEdit {
25937 new_text: "head".to_string(),
25938 insert: lsp::Range::new(
25939 lsp::Position::new(0, 1),
25940 lsp::Position::new(0, 3),
25941 ),
25942 replace: lsp::Range::new(
25943 lsp::Position::new(0, 1),
25944 lsp::Position::new(0, 3),
25945 ),
25946 },
25947 )),
25948 ..Default::default()
25949 },
25950 ])))
25951 });
25952 editor.update_in(cx, |editor, window, cx| {
25953 editor.show_completions(&ShowCompletions, window, cx);
25954 });
25955 cx.run_until_parked();
25956 completion_handle.next().await.unwrap();
25957 editor.update(cx, |editor, _| {
25958 assert!(
25959 editor.context_menu_visible(),
25960 "Completion menu should be visible"
25961 );
25962 });
25963 editor.update_in(cx, |editor, window, cx| {
25964 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
25965 });
25966 cx.executor().run_until_parked();
25967 editor.update(cx, |editor, cx| {
25968 assert_eq!(editor.text(cx), "<head></head>");
25969 });
25970}
25971
25972#[gpui::test]
25973async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
25974 init_test(cx, |_| {});
25975
25976 let mut cx = EditorTestContext::new(cx).await;
25977 let language = Arc::new(Language::new(
25978 LanguageConfig {
25979 name: "TSX".into(),
25980 matcher: LanguageMatcher {
25981 path_suffixes: vec!["tsx".to_string()],
25982 ..LanguageMatcher::default()
25983 },
25984 brackets: BracketPairConfig {
25985 pairs: vec![BracketPair {
25986 start: "<".into(),
25987 end: ">".into(),
25988 close: true,
25989 ..Default::default()
25990 }],
25991 ..Default::default()
25992 },
25993 linked_edit_characters: HashSet::from_iter(['.']),
25994 ..Default::default()
25995 },
25996 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
25997 ));
25998 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25999
26000 // Test typing > does not extend linked pair
26001 cx.set_state("<divˇ<div></div>");
26002 cx.update_editor(|editor, _, cx| {
26003 set_linked_edit_ranges(
26004 (Point::new(0, 1), Point::new(0, 4)),
26005 (Point::new(0, 11), Point::new(0, 14)),
26006 editor,
26007 cx,
26008 );
26009 });
26010 cx.update_editor(|editor, window, cx| {
26011 editor.handle_input(">", window, cx);
26012 });
26013 cx.assert_editor_state("<div>ˇ<div></div>");
26014
26015 // Test typing . do extend linked pair
26016 cx.set_state("<Animatedˇ></Animated>");
26017 cx.update_editor(|editor, _, cx| {
26018 set_linked_edit_ranges(
26019 (Point::new(0, 1), Point::new(0, 9)),
26020 (Point::new(0, 12), Point::new(0, 20)),
26021 editor,
26022 cx,
26023 );
26024 });
26025 cx.update_editor(|editor, window, cx| {
26026 editor.handle_input(".", window, cx);
26027 });
26028 cx.assert_editor_state("<Animated.ˇ></Animated.>");
26029 cx.update_editor(|editor, _, cx| {
26030 set_linked_edit_ranges(
26031 (Point::new(0, 1), Point::new(0, 10)),
26032 (Point::new(0, 13), Point::new(0, 21)),
26033 editor,
26034 cx,
26035 );
26036 });
26037 cx.update_editor(|editor, window, cx| {
26038 editor.handle_input("V", window, cx);
26039 });
26040 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
26041}
26042
26043#[gpui::test]
26044async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
26045 init_test(cx, |_| {});
26046
26047 let fs = FakeFs::new(cx.executor());
26048 fs.insert_tree(
26049 path!("/root"),
26050 json!({
26051 "a": {
26052 "main.rs": "fn main() {}",
26053 },
26054 "foo": {
26055 "bar": {
26056 "external_file.rs": "pub mod external {}",
26057 }
26058 }
26059 }),
26060 )
26061 .await;
26062
26063 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
26064 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26065 language_registry.add(rust_lang());
26066 let _fake_servers = language_registry.register_fake_lsp(
26067 "Rust",
26068 FakeLspAdapter {
26069 ..FakeLspAdapter::default()
26070 },
26071 );
26072 let (workspace, cx) =
26073 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26074 let worktree_id = workspace.update(cx, |workspace, cx| {
26075 workspace.project().update(cx, |project, cx| {
26076 project.worktrees(cx).next().unwrap().read(cx).id()
26077 })
26078 });
26079
26080 let assert_language_servers_count =
26081 |expected: usize, context: &str, cx: &mut VisualTestContext| {
26082 project.update(cx, |project, cx| {
26083 let current = project
26084 .lsp_store()
26085 .read(cx)
26086 .as_local()
26087 .unwrap()
26088 .language_servers
26089 .len();
26090 assert_eq!(expected, current, "{context}");
26091 });
26092 };
26093
26094 assert_language_servers_count(
26095 0,
26096 "No servers should be running before any file is open",
26097 cx,
26098 );
26099 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
26100 let main_editor = workspace
26101 .update_in(cx, |workspace, window, cx| {
26102 workspace.open_path(
26103 (worktree_id, rel_path("main.rs")),
26104 Some(pane.downgrade()),
26105 true,
26106 window,
26107 cx,
26108 )
26109 })
26110 .unwrap()
26111 .await
26112 .downcast::<Editor>()
26113 .unwrap();
26114 pane.update(cx, |pane, cx| {
26115 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
26116 open_editor.update(cx, |editor, cx| {
26117 assert_eq!(
26118 editor.display_text(cx),
26119 "fn main() {}",
26120 "Original main.rs text on initial open",
26121 );
26122 });
26123 assert_eq!(open_editor, main_editor);
26124 });
26125 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
26126
26127 let external_editor = workspace
26128 .update_in(cx, |workspace, window, cx| {
26129 workspace.open_abs_path(
26130 PathBuf::from("/root/foo/bar/external_file.rs"),
26131 OpenOptions::default(),
26132 window,
26133 cx,
26134 )
26135 })
26136 .await
26137 .expect("opening external file")
26138 .downcast::<Editor>()
26139 .expect("downcasted external file's open element to editor");
26140 pane.update(cx, |pane, cx| {
26141 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
26142 open_editor.update(cx, |editor, cx| {
26143 assert_eq!(
26144 editor.display_text(cx),
26145 "pub mod external {}",
26146 "External file is open now",
26147 );
26148 });
26149 assert_eq!(open_editor, external_editor);
26150 });
26151 assert_language_servers_count(
26152 1,
26153 "Second, external, *.rs file should join the existing server",
26154 cx,
26155 );
26156
26157 pane.update_in(cx, |pane, window, cx| {
26158 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26159 })
26160 .await
26161 .unwrap();
26162 pane.update_in(cx, |pane, window, cx| {
26163 pane.navigate_backward(&Default::default(), window, cx);
26164 });
26165 cx.run_until_parked();
26166 pane.update(cx, |pane, cx| {
26167 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
26168 open_editor.update(cx, |editor, cx| {
26169 assert_eq!(
26170 editor.display_text(cx),
26171 "pub mod external {}",
26172 "External file is open now",
26173 );
26174 });
26175 });
26176 assert_language_servers_count(
26177 1,
26178 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
26179 cx,
26180 );
26181
26182 cx.update(|_, cx| {
26183 workspace::reload(cx);
26184 });
26185 assert_language_servers_count(
26186 1,
26187 "After reloading the worktree with local and external files opened, only one project should be started",
26188 cx,
26189 );
26190}
26191
26192#[gpui::test]
26193async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
26194 init_test(cx, |_| {});
26195
26196 let mut cx = EditorTestContext::new(cx).await;
26197 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
26198 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26199
26200 // test cursor move to start of each line on tab
26201 // for `if`, `elif`, `else`, `while`, `with` and `for`
26202 cx.set_state(indoc! {"
26203 def main():
26204 ˇ for item in items:
26205 ˇ while item.active:
26206 ˇ if item.value > 10:
26207 ˇ continue
26208 ˇ elif item.value < 0:
26209 ˇ break
26210 ˇ else:
26211 ˇ with item.context() as ctx:
26212 ˇ yield count
26213 ˇ else:
26214 ˇ log('while else')
26215 ˇ else:
26216 ˇ log('for else')
26217 "});
26218 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26219 cx.wait_for_autoindent_applied().await;
26220 cx.assert_editor_state(indoc! {"
26221 def main():
26222 ˇfor item in items:
26223 ˇwhile item.active:
26224 ˇif item.value > 10:
26225 ˇcontinue
26226 ˇelif item.value < 0:
26227 ˇbreak
26228 ˇelse:
26229 ˇwith item.context() as ctx:
26230 ˇyield count
26231 ˇelse:
26232 ˇlog('while else')
26233 ˇelse:
26234 ˇlog('for else')
26235 "});
26236 // test relative indent is preserved when tab
26237 // for `if`, `elif`, `else`, `while`, `with` and `for`
26238 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26239 cx.wait_for_autoindent_applied().await;
26240 cx.assert_editor_state(indoc! {"
26241 def main():
26242 ˇfor item in items:
26243 ˇwhile item.active:
26244 ˇif item.value > 10:
26245 ˇcontinue
26246 ˇelif item.value < 0:
26247 ˇbreak
26248 ˇelse:
26249 ˇwith item.context() as ctx:
26250 ˇyield count
26251 ˇelse:
26252 ˇlog('while else')
26253 ˇelse:
26254 ˇlog('for else')
26255 "});
26256
26257 // test cursor move to start of each line on tab
26258 // for `try`, `except`, `else`, `finally`, `match` and `def`
26259 cx.set_state(indoc! {"
26260 def main():
26261 ˇ try:
26262 ˇ fetch()
26263 ˇ except ValueError:
26264 ˇ handle_error()
26265 ˇ else:
26266 ˇ match value:
26267 ˇ case _:
26268 ˇ finally:
26269 ˇ def status():
26270 ˇ return 0
26271 "});
26272 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26273 cx.wait_for_autoindent_applied().await;
26274 cx.assert_editor_state(indoc! {"
26275 def main():
26276 ˇtry:
26277 ˇfetch()
26278 ˇexcept ValueError:
26279 ˇhandle_error()
26280 ˇelse:
26281 ˇmatch value:
26282 ˇcase _:
26283 ˇfinally:
26284 ˇdef status():
26285 ˇreturn 0
26286 "});
26287 // test relative indent is preserved when tab
26288 // for `try`, `except`, `else`, `finally`, `match` and `def`
26289 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26290 cx.wait_for_autoindent_applied().await;
26291 cx.assert_editor_state(indoc! {"
26292 def main():
26293 ˇtry:
26294 ˇfetch()
26295 ˇexcept ValueError:
26296 ˇhandle_error()
26297 ˇelse:
26298 ˇmatch value:
26299 ˇcase _:
26300 ˇfinally:
26301 ˇdef status():
26302 ˇreturn 0
26303 "});
26304}
26305
26306#[gpui::test]
26307async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
26308 init_test(cx, |_| {});
26309
26310 let mut cx = EditorTestContext::new(cx).await;
26311 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
26312 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26313
26314 // test `else` auto outdents when typed inside `if` block
26315 cx.set_state(indoc! {"
26316 def main():
26317 if i == 2:
26318 return
26319 ˇ
26320 "});
26321 cx.update_editor(|editor, window, cx| {
26322 editor.handle_input("else:", window, cx);
26323 });
26324 cx.wait_for_autoindent_applied().await;
26325 cx.assert_editor_state(indoc! {"
26326 def main():
26327 if i == 2:
26328 return
26329 else:ˇ
26330 "});
26331
26332 // test `except` auto outdents when typed inside `try` block
26333 cx.set_state(indoc! {"
26334 def main():
26335 try:
26336 i = 2
26337 ˇ
26338 "});
26339 cx.update_editor(|editor, window, cx| {
26340 editor.handle_input("except:", window, cx);
26341 });
26342 cx.wait_for_autoindent_applied().await;
26343 cx.assert_editor_state(indoc! {"
26344 def main():
26345 try:
26346 i = 2
26347 except:ˇ
26348 "});
26349
26350 // test `else` auto outdents when typed inside `except` block
26351 cx.set_state(indoc! {"
26352 def main():
26353 try:
26354 i = 2
26355 except:
26356 j = 2
26357 ˇ
26358 "});
26359 cx.update_editor(|editor, window, cx| {
26360 editor.handle_input("else:", window, cx);
26361 });
26362 cx.wait_for_autoindent_applied().await;
26363 cx.assert_editor_state(indoc! {"
26364 def main():
26365 try:
26366 i = 2
26367 except:
26368 j = 2
26369 else:ˇ
26370 "});
26371
26372 // test `finally` auto outdents when typed inside `else` block
26373 cx.set_state(indoc! {"
26374 def main():
26375 try:
26376 i = 2
26377 except:
26378 j = 2
26379 else:
26380 k = 2
26381 ˇ
26382 "});
26383 cx.update_editor(|editor, window, cx| {
26384 editor.handle_input("finally:", window, cx);
26385 });
26386 cx.wait_for_autoindent_applied().await;
26387 cx.assert_editor_state(indoc! {"
26388 def main():
26389 try:
26390 i = 2
26391 except:
26392 j = 2
26393 else:
26394 k = 2
26395 finally:ˇ
26396 "});
26397
26398 // test `else` does not outdents when typed inside `except` block right after for block
26399 cx.set_state(indoc! {"
26400 def main():
26401 try:
26402 i = 2
26403 except:
26404 for i in range(n):
26405 pass
26406 ˇ
26407 "});
26408 cx.update_editor(|editor, window, cx| {
26409 editor.handle_input("else:", window, cx);
26410 });
26411 cx.wait_for_autoindent_applied().await;
26412 cx.assert_editor_state(indoc! {"
26413 def main():
26414 try:
26415 i = 2
26416 except:
26417 for i in range(n):
26418 pass
26419 else:ˇ
26420 "});
26421
26422 // test `finally` auto outdents when typed inside `else` block right after for block
26423 cx.set_state(indoc! {"
26424 def main():
26425 try:
26426 i = 2
26427 except:
26428 j = 2
26429 else:
26430 for i in range(n):
26431 pass
26432 ˇ
26433 "});
26434 cx.update_editor(|editor, window, cx| {
26435 editor.handle_input("finally:", window, cx);
26436 });
26437 cx.wait_for_autoindent_applied().await;
26438 cx.assert_editor_state(indoc! {"
26439 def main():
26440 try:
26441 i = 2
26442 except:
26443 j = 2
26444 else:
26445 for i in range(n):
26446 pass
26447 finally:ˇ
26448 "});
26449
26450 // test `except` outdents to inner "try" block
26451 cx.set_state(indoc! {"
26452 def main():
26453 try:
26454 i = 2
26455 if i == 2:
26456 try:
26457 i = 3
26458 ˇ
26459 "});
26460 cx.update_editor(|editor, window, cx| {
26461 editor.handle_input("except:", window, cx);
26462 });
26463 cx.wait_for_autoindent_applied().await;
26464 cx.assert_editor_state(indoc! {"
26465 def main():
26466 try:
26467 i = 2
26468 if i == 2:
26469 try:
26470 i = 3
26471 except:ˇ
26472 "});
26473
26474 // test `except` outdents to outer "try" block
26475 cx.set_state(indoc! {"
26476 def main():
26477 try:
26478 i = 2
26479 if i == 2:
26480 try:
26481 i = 3
26482 ˇ
26483 "});
26484 cx.update_editor(|editor, window, cx| {
26485 editor.handle_input("except:", window, cx);
26486 });
26487 cx.wait_for_autoindent_applied().await;
26488 cx.assert_editor_state(indoc! {"
26489 def main():
26490 try:
26491 i = 2
26492 if i == 2:
26493 try:
26494 i = 3
26495 except:ˇ
26496 "});
26497
26498 // test `else` stays at correct indent when typed after `for` block
26499 cx.set_state(indoc! {"
26500 def main():
26501 for i in range(10):
26502 if i == 3:
26503 break
26504 ˇ
26505 "});
26506 cx.update_editor(|editor, window, cx| {
26507 editor.handle_input("else:", window, cx);
26508 });
26509 cx.wait_for_autoindent_applied().await;
26510 cx.assert_editor_state(indoc! {"
26511 def main():
26512 for i in range(10):
26513 if i == 3:
26514 break
26515 else:ˇ
26516 "});
26517
26518 // test does not outdent on typing after line with square brackets
26519 cx.set_state(indoc! {"
26520 def f() -> list[str]:
26521 ˇ
26522 "});
26523 cx.update_editor(|editor, window, cx| {
26524 editor.handle_input("a", window, cx);
26525 });
26526 cx.wait_for_autoindent_applied().await;
26527 cx.assert_editor_state(indoc! {"
26528 def f() -> list[str]:
26529 aˇ
26530 "});
26531
26532 // test does not outdent on typing : after case keyword
26533 cx.set_state(indoc! {"
26534 match 1:
26535 caseˇ
26536 "});
26537 cx.update_editor(|editor, window, cx| {
26538 editor.handle_input(":", window, cx);
26539 });
26540 cx.wait_for_autoindent_applied().await;
26541 cx.assert_editor_state(indoc! {"
26542 match 1:
26543 case:ˇ
26544 "});
26545}
26546
26547#[gpui::test]
26548async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
26549 init_test(cx, |_| {});
26550 update_test_language_settings(cx, |settings| {
26551 settings.defaults.extend_comment_on_newline = Some(false);
26552 });
26553 let mut cx = EditorTestContext::new(cx).await;
26554 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
26555 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26556
26557 // test correct indent after newline on comment
26558 cx.set_state(indoc! {"
26559 # COMMENT:ˇ
26560 "});
26561 cx.update_editor(|editor, window, cx| {
26562 editor.newline(&Newline, window, cx);
26563 });
26564 cx.wait_for_autoindent_applied().await;
26565 cx.assert_editor_state(indoc! {"
26566 # COMMENT:
26567 ˇ
26568 "});
26569
26570 // test correct indent after newline in brackets
26571 cx.set_state(indoc! {"
26572 {ˇ}
26573 "});
26574 cx.update_editor(|editor, window, cx| {
26575 editor.newline(&Newline, window, cx);
26576 });
26577 cx.wait_for_autoindent_applied().await;
26578 cx.assert_editor_state(indoc! {"
26579 {
26580 ˇ
26581 }
26582 "});
26583
26584 cx.set_state(indoc! {"
26585 (ˇ)
26586 "});
26587 cx.update_editor(|editor, window, cx| {
26588 editor.newline(&Newline, window, cx);
26589 });
26590 cx.run_until_parked();
26591 cx.assert_editor_state(indoc! {"
26592 (
26593 ˇ
26594 )
26595 "});
26596
26597 // do not indent after empty lists or dictionaries
26598 cx.set_state(indoc! {"
26599 a = []ˇ
26600 "});
26601 cx.update_editor(|editor, window, cx| {
26602 editor.newline(&Newline, window, cx);
26603 });
26604 cx.run_until_parked();
26605 cx.assert_editor_state(indoc! {"
26606 a = []
26607 ˇ
26608 "});
26609}
26610
26611#[gpui::test]
26612async fn test_python_indent_in_markdown(cx: &mut TestAppContext) {
26613 init_test(cx, |_| {});
26614
26615 let language_registry = Arc::new(language::LanguageRegistry::test(cx.executor()));
26616 let python_lang = languages::language("python", tree_sitter_python::LANGUAGE.into());
26617 language_registry.add(markdown_lang());
26618 language_registry.add(python_lang);
26619
26620 let mut cx = EditorTestContext::new(cx).await;
26621 cx.update_buffer(|buffer, cx| {
26622 buffer.set_language_registry(language_registry);
26623 buffer.set_language(Some(markdown_lang()), cx);
26624 });
26625
26626 // Test that `else:` correctly outdents to match `if:` inside the Python code block
26627 cx.set_state(indoc! {"
26628 # Heading
26629
26630 ```python
26631 def main():
26632 if condition:
26633 pass
26634 ˇ
26635 ```
26636 "});
26637 cx.update_editor(|editor, window, cx| {
26638 editor.handle_input("else:", window, cx);
26639 });
26640 cx.run_until_parked();
26641 cx.assert_editor_state(indoc! {"
26642 # Heading
26643
26644 ```python
26645 def main():
26646 if condition:
26647 pass
26648 else:ˇ
26649 ```
26650 "});
26651}
26652
26653#[gpui::test]
26654async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
26655 init_test(cx, |_| {});
26656
26657 let mut cx = EditorTestContext::new(cx).await;
26658 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26659 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26660
26661 // test cursor move to start of each line on tab
26662 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
26663 cx.set_state(indoc! {"
26664 function main() {
26665 ˇ for item in $items; do
26666 ˇ while [ -n \"$item\" ]; do
26667 ˇ if [ \"$value\" -gt 10 ]; then
26668 ˇ continue
26669 ˇ elif [ \"$value\" -lt 0 ]; then
26670 ˇ break
26671 ˇ else
26672 ˇ echo \"$item\"
26673 ˇ fi
26674 ˇ done
26675 ˇ done
26676 ˇ}
26677 "});
26678 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26679 cx.wait_for_autoindent_applied().await;
26680 cx.assert_editor_state(indoc! {"
26681 function main() {
26682 ˇfor item in $items; do
26683 ˇwhile [ -n \"$item\" ]; do
26684 ˇif [ \"$value\" -gt 10 ]; then
26685 ˇcontinue
26686 ˇelif [ \"$value\" -lt 0 ]; then
26687 ˇbreak
26688 ˇelse
26689 ˇecho \"$item\"
26690 ˇfi
26691 ˇdone
26692 ˇdone
26693 ˇ}
26694 "});
26695 // test relative indent is preserved when tab
26696 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26697 cx.wait_for_autoindent_applied().await;
26698 cx.assert_editor_state(indoc! {"
26699 function main() {
26700 ˇfor item in $items; do
26701 ˇwhile [ -n \"$item\" ]; do
26702 ˇif [ \"$value\" -gt 10 ]; then
26703 ˇcontinue
26704 ˇelif [ \"$value\" -lt 0 ]; then
26705 ˇbreak
26706 ˇelse
26707 ˇecho \"$item\"
26708 ˇfi
26709 ˇdone
26710 ˇdone
26711 ˇ}
26712 "});
26713
26714 // test cursor move to start of each line on tab
26715 // for `case` statement with patterns
26716 cx.set_state(indoc! {"
26717 function handle() {
26718 ˇ case \"$1\" in
26719 ˇ start)
26720 ˇ echo \"a\"
26721 ˇ ;;
26722 ˇ stop)
26723 ˇ echo \"b\"
26724 ˇ ;;
26725 ˇ *)
26726 ˇ echo \"c\"
26727 ˇ ;;
26728 ˇ esac
26729 ˇ}
26730 "});
26731 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
26732 cx.wait_for_autoindent_applied().await;
26733 cx.assert_editor_state(indoc! {"
26734 function handle() {
26735 ˇcase \"$1\" in
26736 ˇstart)
26737 ˇecho \"a\"
26738 ˇ;;
26739 ˇstop)
26740 ˇecho \"b\"
26741 ˇ;;
26742 ˇ*)
26743 ˇecho \"c\"
26744 ˇ;;
26745 ˇesac
26746 ˇ}
26747 "});
26748}
26749
26750#[gpui::test]
26751async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
26752 init_test(cx, |_| {});
26753
26754 let mut cx = EditorTestContext::new(cx).await;
26755 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26756 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26757
26758 // test indents on comment insert
26759 cx.set_state(indoc! {"
26760 function main() {
26761 ˇ for item in $items; do
26762 ˇ while [ -n \"$item\" ]; do
26763 ˇ if [ \"$value\" -gt 10 ]; then
26764 ˇ continue
26765 ˇ elif [ \"$value\" -lt 0 ]; then
26766 ˇ break
26767 ˇ else
26768 ˇ echo \"$item\"
26769 ˇ fi
26770 ˇ done
26771 ˇ done
26772 ˇ}
26773 "});
26774 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
26775 cx.wait_for_autoindent_applied().await;
26776 cx.assert_editor_state(indoc! {"
26777 function main() {
26778 #ˇ for item in $items; do
26779 #ˇ while [ -n \"$item\" ]; do
26780 #ˇ if [ \"$value\" -gt 10 ]; then
26781 #ˇ continue
26782 #ˇ elif [ \"$value\" -lt 0 ]; then
26783 #ˇ break
26784 #ˇ else
26785 #ˇ echo \"$item\"
26786 #ˇ fi
26787 #ˇ done
26788 #ˇ done
26789 #ˇ}
26790 "});
26791}
26792
26793#[gpui::test]
26794async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
26795 init_test(cx, |_| {});
26796
26797 let mut cx = EditorTestContext::new(cx).await;
26798 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26799 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26800
26801 // test `else` auto outdents when typed inside `if` block
26802 cx.set_state(indoc! {"
26803 if [ \"$1\" = \"test\" ]; then
26804 echo \"foo bar\"
26805 ˇ
26806 "});
26807 cx.update_editor(|editor, window, cx| {
26808 editor.handle_input("else", window, cx);
26809 });
26810 cx.wait_for_autoindent_applied().await;
26811 cx.assert_editor_state(indoc! {"
26812 if [ \"$1\" = \"test\" ]; then
26813 echo \"foo bar\"
26814 elseˇ
26815 "});
26816
26817 // test `elif` auto outdents when typed inside `if` block
26818 cx.set_state(indoc! {"
26819 if [ \"$1\" = \"test\" ]; then
26820 echo \"foo bar\"
26821 ˇ
26822 "});
26823 cx.update_editor(|editor, window, cx| {
26824 editor.handle_input("elif", window, cx);
26825 });
26826 cx.wait_for_autoindent_applied().await;
26827 cx.assert_editor_state(indoc! {"
26828 if [ \"$1\" = \"test\" ]; then
26829 echo \"foo bar\"
26830 elifˇ
26831 "});
26832
26833 // test `fi` auto outdents when typed inside `else` block
26834 cx.set_state(indoc! {"
26835 if [ \"$1\" = \"test\" ]; then
26836 echo \"foo bar\"
26837 else
26838 echo \"bar baz\"
26839 ˇ
26840 "});
26841 cx.update_editor(|editor, window, cx| {
26842 editor.handle_input("fi", window, cx);
26843 });
26844 cx.wait_for_autoindent_applied().await;
26845 cx.assert_editor_state(indoc! {"
26846 if [ \"$1\" = \"test\" ]; then
26847 echo \"foo bar\"
26848 else
26849 echo \"bar baz\"
26850 fiˇ
26851 "});
26852
26853 // test `done` auto outdents when typed inside `while` block
26854 cx.set_state(indoc! {"
26855 while read line; do
26856 echo \"$line\"
26857 ˇ
26858 "});
26859 cx.update_editor(|editor, window, cx| {
26860 editor.handle_input("done", window, cx);
26861 });
26862 cx.wait_for_autoindent_applied().await;
26863 cx.assert_editor_state(indoc! {"
26864 while read line; do
26865 echo \"$line\"
26866 doneˇ
26867 "});
26868
26869 // test `done` auto outdents when typed inside `for` block
26870 cx.set_state(indoc! {"
26871 for file in *.txt; do
26872 cat \"$file\"
26873 ˇ
26874 "});
26875 cx.update_editor(|editor, window, cx| {
26876 editor.handle_input("done", window, cx);
26877 });
26878 cx.wait_for_autoindent_applied().await;
26879 cx.assert_editor_state(indoc! {"
26880 for file in *.txt; do
26881 cat \"$file\"
26882 doneˇ
26883 "});
26884
26885 // test `esac` auto outdents when typed inside `case` block
26886 cx.set_state(indoc! {"
26887 case \"$1\" in
26888 start)
26889 echo \"foo bar\"
26890 ;;
26891 stop)
26892 echo \"bar baz\"
26893 ;;
26894 ˇ
26895 "});
26896 cx.update_editor(|editor, window, cx| {
26897 editor.handle_input("esac", window, cx);
26898 });
26899 cx.wait_for_autoindent_applied().await;
26900 cx.assert_editor_state(indoc! {"
26901 case \"$1\" in
26902 start)
26903 echo \"foo bar\"
26904 ;;
26905 stop)
26906 echo \"bar baz\"
26907 ;;
26908 esacˇ
26909 "});
26910
26911 // test `*)` auto outdents when typed inside `case` block
26912 cx.set_state(indoc! {"
26913 case \"$1\" in
26914 start)
26915 echo \"foo bar\"
26916 ;;
26917 ˇ
26918 "});
26919 cx.update_editor(|editor, window, cx| {
26920 editor.handle_input("*)", window, cx);
26921 });
26922 cx.wait_for_autoindent_applied().await;
26923 cx.assert_editor_state(indoc! {"
26924 case \"$1\" in
26925 start)
26926 echo \"foo bar\"
26927 ;;
26928 *)ˇ
26929 "});
26930
26931 // test `fi` outdents to correct level with nested if blocks
26932 cx.set_state(indoc! {"
26933 if [ \"$1\" = \"test\" ]; then
26934 echo \"outer if\"
26935 if [ \"$2\" = \"debug\" ]; then
26936 echo \"inner if\"
26937 ˇ
26938 "});
26939 cx.update_editor(|editor, window, cx| {
26940 editor.handle_input("fi", window, cx);
26941 });
26942 cx.wait_for_autoindent_applied().await;
26943 cx.assert_editor_state(indoc! {"
26944 if [ \"$1\" = \"test\" ]; then
26945 echo \"outer if\"
26946 if [ \"$2\" = \"debug\" ]; then
26947 echo \"inner if\"
26948 fiˇ
26949 "});
26950}
26951
26952#[gpui::test]
26953async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
26954 init_test(cx, |_| {});
26955 update_test_language_settings(cx, |settings| {
26956 settings.defaults.extend_comment_on_newline = Some(false);
26957 });
26958 let mut cx = EditorTestContext::new(cx).await;
26959 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
26960 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
26961
26962 // test correct indent after newline on comment
26963 cx.set_state(indoc! {"
26964 # COMMENT:ˇ
26965 "});
26966 cx.update_editor(|editor, window, cx| {
26967 editor.newline(&Newline, window, cx);
26968 });
26969 cx.wait_for_autoindent_applied().await;
26970 cx.assert_editor_state(indoc! {"
26971 # COMMENT:
26972 ˇ
26973 "});
26974
26975 // test correct indent after newline after `then`
26976 cx.set_state(indoc! {"
26977
26978 if [ \"$1\" = \"test\" ]; thenˇ
26979 "});
26980 cx.update_editor(|editor, window, cx| {
26981 editor.newline(&Newline, window, cx);
26982 });
26983 cx.wait_for_autoindent_applied().await;
26984 cx.assert_editor_state(indoc! {"
26985
26986 if [ \"$1\" = \"test\" ]; then
26987 ˇ
26988 "});
26989
26990 // test correct indent after newline after `else`
26991 cx.set_state(indoc! {"
26992 if [ \"$1\" = \"test\" ]; then
26993 elseˇ
26994 "});
26995 cx.update_editor(|editor, window, cx| {
26996 editor.newline(&Newline, window, cx);
26997 });
26998 cx.wait_for_autoindent_applied().await;
26999 cx.assert_editor_state(indoc! {"
27000 if [ \"$1\" = \"test\" ]; then
27001 else
27002 ˇ
27003 "});
27004
27005 // test correct indent after newline after `elif`
27006 cx.set_state(indoc! {"
27007 if [ \"$1\" = \"test\" ]; then
27008 elifˇ
27009 "});
27010 cx.update_editor(|editor, window, cx| {
27011 editor.newline(&Newline, window, cx);
27012 });
27013 cx.wait_for_autoindent_applied().await;
27014 cx.assert_editor_state(indoc! {"
27015 if [ \"$1\" = \"test\" ]; then
27016 elif
27017 ˇ
27018 "});
27019
27020 // test correct indent after newline after `do`
27021 cx.set_state(indoc! {"
27022 for file in *.txt; doˇ
27023 "});
27024 cx.update_editor(|editor, window, cx| {
27025 editor.newline(&Newline, window, cx);
27026 });
27027 cx.wait_for_autoindent_applied().await;
27028 cx.assert_editor_state(indoc! {"
27029 for file in *.txt; do
27030 ˇ
27031 "});
27032
27033 // test correct indent after newline after case pattern
27034 cx.set_state(indoc! {"
27035 case \"$1\" in
27036 start)ˇ
27037 "});
27038 cx.update_editor(|editor, window, cx| {
27039 editor.newline(&Newline, window, cx);
27040 });
27041 cx.wait_for_autoindent_applied().await;
27042 cx.assert_editor_state(indoc! {"
27043 case \"$1\" in
27044 start)
27045 ˇ
27046 "});
27047
27048 // test correct indent after newline after case pattern
27049 cx.set_state(indoc! {"
27050 case \"$1\" in
27051 start)
27052 ;;
27053 *)ˇ
27054 "});
27055 cx.update_editor(|editor, window, cx| {
27056 editor.newline(&Newline, window, cx);
27057 });
27058 cx.wait_for_autoindent_applied().await;
27059 cx.assert_editor_state(indoc! {"
27060 case \"$1\" in
27061 start)
27062 ;;
27063 *)
27064 ˇ
27065 "});
27066
27067 // test correct indent after newline after function opening brace
27068 cx.set_state(indoc! {"
27069 function test() {ˇ}
27070 "});
27071 cx.update_editor(|editor, window, cx| {
27072 editor.newline(&Newline, window, cx);
27073 });
27074 cx.wait_for_autoindent_applied().await;
27075 cx.assert_editor_state(indoc! {"
27076 function test() {
27077 ˇ
27078 }
27079 "});
27080
27081 // test no extra indent after semicolon on same line
27082 cx.set_state(indoc! {"
27083 echo \"test\";ˇ
27084 "});
27085 cx.update_editor(|editor, window, cx| {
27086 editor.newline(&Newline, window, cx);
27087 });
27088 cx.wait_for_autoindent_applied().await;
27089 cx.assert_editor_state(indoc! {"
27090 echo \"test\";
27091 ˇ
27092 "});
27093}
27094
27095fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
27096 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
27097 point..point
27098}
27099
27100#[track_caller]
27101fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
27102 let (text, ranges) = marked_text_ranges(marked_text, true);
27103 assert_eq!(editor.text(cx), text);
27104 assert_eq!(
27105 editor.selections.ranges(&editor.display_snapshot(cx)),
27106 ranges
27107 .iter()
27108 .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end))
27109 .collect::<Vec<_>>(),
27110 "Assert selections are {}",
27111 marked_text
27112 );
27113}
27114
27115pub fn handle_signature_help_request(
27116 cx: &mut EditorLspTestContext,
27117 mocked_response: lsp::SignatureHelp,
27118) -> impl Future<Output = ()> + use<> {
27119 let mut request =
27120 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
27121 let mocked_response = mocked_response.clone();
27122 async move { Ok(Some(mocked_response)) }
27123 });
27124
27125 async move {
27126 request.next().await;
27127 }
27128}
27129
27130#[track_caller]
27131pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
27132 cx.update_editor(|editor, _, _| {
27133 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
27134 let entries = menu.entries.borrow();
27135 let entries = entries
27136 .iter()
27137 .map(|entry| entry.string.as_str())
27138 .collect::<Vec<_>>();
27139 assert_eq!(entries, expected);
27140 } else {
27141 panic!("Expected completions menu");
27142 }
27143 });
27144}
27145
27146#[gpui::test]
27147async fn test_mixed_completions_with_multi_word_snippet(cx: &mut TestAppContext) {
27148 init_test(cx, |_| {});
27149 let mut cx = EditorLspTestContext::new_rust(
27150 lsp::ServerCapabilities {
27151 completion_provider: Some(lsp::CompletionOptions {
27152 ..Default::default()
27153 }),
27154 ..Default::default()
27155 },
27156 cx,
27157 )
27158 .await;
27159 cx.lsp
27160 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
27161 Ok(Some(lsp::CompletionResponse::Array(vec![
27162 lsp::CompletionItem {
27163 label: "unsafe".into(),
27164 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
27165 range: lsp::Range {
27166 start: lsp::Position {
27167 line: 0,
27168 character: 9,
27169 },
27170 end: lsp::Position {
27171 line: 0,
27172 character: 11,
27173 },
27174 },
27175 new_text: "unsafe".to_string(),
27176 })),
27177 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
27178 ..Default::default()
27179 },
27180 ])))
27181 });
27182
27183 cx.update_editor(|editor, _, cx| {
27184 editor.project().unwrap().update(cx, |project, cx| {
27185 project.snippets().update(cx, |snippets, _cx| {
27186 snippets.add_snippet_for_test(
27187 None,
27188 PathBuf::from("test_snippets.json"),
27189 vec![
27190 Arc::new(project::snippet_provider::Snippet {
27191 prefix: vec![
27192 "unlimited word count".to_string(),
27193 "unlimit word count".to_string(),
27194 "unlimited unknown".to_string(),
27195 ],
27196 body: "this is many words".to_string(),
27197 description: Some("description".to_string()),
27198 name: "multi-word snippet test".to_string(),
27199 }),
27200 Arc::new(project::snippet_provider::Snippet {
27201 prefix: vec!["unsnip".to_string(), "@few".to_string()],
27202 body: "fewer words".to_string(),
27203 description: Some("alt description".to_string()),
27204 name: "other name".to_string(),
27205 }),
27206 Arc::new(project::snippet_provider::Snippet {
27207 prefix: vec!["ab aa".to_string()],
27208 body: "abcd".to_string(),
27209 description: None,
27210 name: "alphabet".to_string(),
27211 }),
27212 ],
27213 );
27214 });
27215 })
27216 });
27217
27218 let get_completions = |cx: &mut EditorLspTestContext| {
27219 cx.update_editor(|editor, _, _| match &*editor.context_menu.borrow() {
27220 Some(CodeContextMenu::Completions(context_menu)) => {
27221 let entries = context_menu.entries.borrow();
27222 entries
27223 .iter()
27224 .map(|entry| entry.string.clone())
27225 .collect_vec()
27226 }
27227 _ => vec![],
27228 })
27229 };
27230
27231 // snippets:
27232 // @foo
27233 // foo bar
27234 //
27235 // when typing:
27236 //
27237 // when typing:
27238 // - if I type a symbol "open the completions with snippets only"
27239 // - if I type a word character "open the completions menu" (if it had been open snippets only, clear it out)
27240 //
27241 // stuff we need:
27242 // - filtering logic change?
27243 // - remember how far back the completion started.
27244
27245 let test_cases: &[(&str, &[&str])] = &[
27246 (
27247 "un",
27248 &[
27249 "unsafe",
27250 "unlimit word count",
27251 "unlimited unknown",
27252 "unlimited word count",
27253 "unsnip",
27254 ],
27255 ),
27256 (
27257 "u ",
27258 &[
27259 "unlimit word count",
27260 "unlimited unknown",
27261 "unlimited word count",
27262 ],
27263 ),
27264 ("u a", &["ab aa", "unsafe"]), // unsAfe
27265 (
27266 "u u",
27267 &[
27268 "unsafe",
27269 "unlimit word count",
27270 "unlimited unknown", // ranked highest among snippets
27271 "unlimited word count",
27272 "unsnip",
27273 ],
27274 ),
27275 ("uw c", &["unlimit word count", "unlimited word count"]),
27276 (
27277 "u w",
27278 &[
27279 "unlimit word count",
27280 "unlimited word count",
27281 "unlimited unknown",
27282 ],
27283 ),
27284 ("u w ", &["unlimit word count", "unlimited word count"]),
27285 (
27286 "u ",
27287 &[
27288 "unlimit word count",
27289 "unlimited unknown",
27290 "unlimited word count",
27291 ],
27292 ),
27293 ("wor", &[]),
27294 ("uf", &["unsafe"]),
27295 ("af", &["unsafe"]),
27296 ("afu", &[]),
27297 (
27298 "ue",
27299 &["unsafe", "unlimited unknown", "unlimited word count"],
27300 ),
27301 ("@", &["@few"]),
27302 ("@few", &["@few"]),
27303 ("@ ", &[]),
27304 ("a@", &["@few"]),
27305 ("a@f", &["@few", "unsafe"]),
27306 ("a@fw", &["@few"]),
27307 ("a", &["ab aa", "unsafe"]),
27308 ("aa", &["ab aa"]),
27309 ("aaa", &["ab aa"]),
27310 ("ab", &["ab aa"]),
27311 ("ab ", &["ab aa"]),
27312 ("ab a", &["ab aa", "unsafe"]),
27313 ("ab ab", &["ab aa"]),
27314 ("ab ab aa", &["ab aa"]),
27315 ];
27316
27317 for &(input_to_simulate, expected_completions) in test_cases {
27318 cx.set_state("fn a() { ˇ }\n");
27319 for c in input_to_simulate.split("") {
27320 cx.simulate_input(c);
27321 cx.run_until_parked();
27322 }
27323 let expected_completions = expected_completions
27324 .iter()
27325 .map(|s| s.to_string())
27326 .collect_vec();
27327 assert_eq!(
27328 get_completions(&mut cx),
27329 expected_completions,
27330 "< actual / expected >, input = {input_to_simulate:?}",
27331 );
27332 }
27333}
27334
27335/// Handle completion request passing a marked string specifying where the completion
27336/// should be triggered from using '|' character, what range should be replaced, and what completions
27337/// should be returned using '<' and '>' to delimit the range.
27338///
27339/// Also see `handle_completion_request_with_insert_and_replace`.
27340#[track_caller]
27341pub fn handle_completion_request(
27342 marked_string: &str,
27343 completions: Vec<&'static str>,
27344 is_incomplete: bool,
27345 counter: Arc<AtomicUsize>,
27346 cx: &mut EditorLspTestContext,
27347) -> impl Future<Output = ()> {
27348 let complete_from_marker: TextRangeMarker = '|'.into();
27349 let replace_range_marker: TextRangeMarker = ('<', '>').into();
27350 let (_, mut marked_ranges) = marked_text_ranges_by(
27351 marked_string,
27352 vec![complete_from_marker.clone(), replace_range_marker.clone()],
27353 );
27354
27355 let complete_from_position = cx.to_lsp(MultiBufferOffset(
27356 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
27357 ));
27358 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
27359 let replace_range =
27360 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
27361
27362 let mut request =
27363 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
27364 let completions = completions.clone();
27365 counter.fetch_add(1, atomic::Ordering::Release);
27366 async move {
27367 assert_eq!(params.text_document_position.text_document.uri, url.clone());
27368 assert_eq!(
27369 params.text_document_position.position,
27370 complete_from_position
27371 );
27372 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
27373 is_incomplete,
27374 item_defaults: None,
27375 items: completions
27376 .iter()
27377 .map(|completion_text| lsp::CompletionItem {
27378 label: completion_text.to_string(),
27379 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
27380 range: replace_range,
27381 new_text: completion_text.to_string(),
27382 })),
27383 ..Default::default()
27384 })
27385 .collect(),
27386 })))
27387 }
27388 });
27389
27390 async move {
27391 request.next().await;
27392 }
27393}
27394
27395/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
27396/// given instead, which also contains an `insert` range.
27397///
27398/// This function uses markers to define ranges:
27399/// - `|` marks the cursor position
27400/// - `<>` marks the replace range
27401/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
27402pub fn handle_completion_request_with_insert_and_replace(
27403 cx: &mut EditorLspTestContext,
27404 marked_string: &str,
27405 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
27406 counter: Arc<AtomicUsize>,
27407) -> impl Future<Output = ()> {
27408 let complete_from_marker: TextRangeMarker = '|'.into();
27409 let replace_range_marker: TextRangeMarker = ('<', '>').into();
27410 let insert_range_marker: TextRangeMarker = ('{', '}').into();
27411
27412 let (_, mut marked_ranges) = marked_text_ranges_by(
27413 marked_string,
27414 vec![
27415 complete_from_marker.clone(),
27416 replace_range_marker.clone(),
27417 insert_range_marker.clone(),
27418 ],
27419 );
27420
27421 let complete_from_position = cx.to_lsp(MultiBufferOffset(
27422 marked_ranges.remove(&complete_from_marker).unwrap()[0].start,
27423 ));
27424 let range = marked_ranges.remove(&replace_range_marker).unwrap()[0].clone();
27425 let replace_range =
27426 cx.to_lsp_range(MultiBufferOffset(range.start)..MultiBufferOffset(range.end));
27427
27428 let insert_range = match marked_ranges.remove(&insert_range_marker) {
27429 Some(ranges) if !ranges.is_empty() => {
27430 let range1 = ranges[0].clone();
27431 cx.to_lsp_range(MultiBufferOffset(range1.start)..MultiBufferOffset(range1.end))
27432 }
27433 _ => lsp::Range {
27434 start: replace_range.start,
27435 end: complete_from_position,
27436 },
27437 };
27438
27439 let mut request =
27440 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
27441 let completions = completions.clone();
27442 counter.fetch_add(1, atomic::Ordering::Release);
27443 async move {
27444 assert_eq!(params.text_document_position.text_document.uri, url.clone());
27445 assert_eq!(
27446 params.text_document_position.position, complete_from_position,
27447 "marker `|` position doesn't match",
27448 );
27449 Ok(Some(lsp::CompletionResponse::Array(
27450 completions
27451 .iter()
27452 .map(|(label, new_text)| lsp::CompletionItem {
27453 label: label.to_string(),
27454 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
27455 lsp::InsertReplaceEdit {
27456 insert: insert_range,
27457 replace: replace_range,
27458 new_text: new_text.to_string(),
27459 },
27460 )),
27461 ..Default::default()
27462 })
27463 .collect(),
27464 )))
27465 }
27466 });
27467
27468 async move {
27469 request.next().await;
27470 }
27471}
27472
27473fn handle_resolve_completion_request(
27474 cx: &mut EditorLspTestContext,
27475 edits: Option<Vec<(&'static str, &'static str)>>,
27476) -> impl Future<Output = ()> {
27477 let edits = edits.map(|edits| {
27478 edits
27479 .iter()
27480 .map(|(marked_string, new_text)| {
27481 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
27482 let replace_range = cx.to_lsp_range(
27483 MultiBufferOffset(marked_ranges[0].start)
27484 ..MultiBufferOffset(marked_ranges[0].end),
27485 );
27486 lsp::TextEdit::new(replace_range, new_text.to_string())
27487 })
27488 .collect::<Vec<_>>()
27489 });
27490
27491 let mut request =
27492 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
27493 let edits = edits.clone();
27494 async move {
27495 Ok(lsp::CompletionItem {
27496 additional_text_edits: edits,
27497 ..Default::default()
27498 })
27499 }
27500 });
27501
27502 async move {
27503 request.next().await;
27504 }
27505}
27506
27507pub(crate) fn update_test_language_settings(
27508 cx: &mut TestAppContext,
27509 f: impl Fn(&mut AllLanguageSettingsContent),
27510) {
27511 cx.update(|cx| {
27512 SettingsStore::update_global(cx, |store, cx| {
27513 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
27514 });
27515 });
27516}
27517
27518pub(crate) fn update_test_project_settings(
27519 cx: &mut TestAppContext,
27520 f: impl Fn(&mut ProjectSettingsContent),
27521) {
27522 cx.update(|cx| {
27523 SettingsStore::update_global(cx, |store, cx| {
27524 store.update_user_settings(cx, |settings| f(&mut settings.project));
27525 });
27526 });
27527}
27528
27529pub(crate) fn update_test_editor_settings(
27530 cx: &mut TestAppContext,
27531 f: impl Fn(&mut EditorSettingsContent),
27532) {
27533 cx.update(|cx| {
27534 SettingsStore::update_global(cx, |store, cx| {
27535 store.update_user_settings(cx, |settings| f(&mut settings.editor));
27536 })
27537 })
27538}
27539
27540pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
27541 cx.update(|cx| {
27542 assets::Assets.load_test_fonts(cx);
27543 let store = SettingsStore::test(cx);
27544 cx.set_global(store);
27545 theme::init(theme::LoadThemes::JustBase, cx);
27546 release_channel::init(semver::Version::new(0, 0, 0), cx);
27547 crate::init(cx);
27548 });
27549 zlog::init_test();
27550 update_test_language_settings(cx, f);
27551}
27552
27553#[track_caller]
27554fn assert_hunk_revert(
27555 not_reverted_text_with_selections: &str,
27556 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
27557 expected_reverted_text_with_selections: &str,
27558 base_text: &str,
27559 cx: &mut EditorLspTestContext,
27560) {
27561 cx.set_state(not_reverted_text_with_selections);
27562 cx.set_head_text(base_text);
27563 cx.executor().run_until_parked();
27564
27565 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
27566 let snapshot = editor.snapshot(window, cx);
27567 let reverted_hunk_statuses = snapshot
27568 .buffer_snapshot()
27569 .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.buffer_snapshot().len())
27570 .map(|hunk| hunk.status().kind)
27571 .collect::<Vec<_>>();
27572
27573 editor.git_restore(&Default::default(), window, cx);
27574 reverted_hunk_statuses
27575 });
27576 cx.executor().run_until_parked();
27577 cx.assert_editor_state(expected_reverted_text_with_selections);
27578 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
27579}
27580
27581#[gpui::test(iterations = 10)]
27582async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
27583 init_test(cx, |_| {});
27584
27585 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
27586 let counter = diagnostic_requests.clone();
27587
27588 let fs = FakeFs::new(cx.executor());
27589 fs.insert_tree(
27590 path!("/a"),
27591 json!({
27592 "first.rs": "fn main() { let a = 5; }",
27593 "second.rs": "// Test file",
27594 }),
27595 )
27596 .await;
27597
27598 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
27599 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
27600 let cx = &mut VisualTestContext::from_window(*workspace, cx);
27601
27602 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
27603 language_registry.add(rust_lang());
27604 let mut fake_servers = language_registry.register_fake_lsp(
27605 "Rust",
27606 FakeLspAdapter {
27607 capabilities: lsp::ServerCapabilities {
27608 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
27609 lsp::DiagnosticOptions {
27610 identifier: None,
27611 inter_file_dependencies: true,
27612 workspace_diagnostics: true,
27613 work_done_progress_options: Default::default(),
27614 },
27615 )),
27616 ..Default::default()
27617 },
27618 ..Default::default()
27619 },
27620 );
27621
27622 let editor = workspace
27623 .update(cx, |workspace, window, cx| {
27624 workspace.open_abs_path(
27625 PathBuf::from(path!("/a/first.rs")),
27626 OpenOptions::default(),
27627 window,
27628 cx,
27629 )
27630 })
27631 .unwrap()
27632 .await
27633 .unwrap()
27634 .downcast::<Editor>()
27635 .unwrap();
27636 let fake_server = fake_servers.next().await.unwrap();
27637 let server_id = fake_server.server.server_id();
27638 let mut first_request = fake_server
27639 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
27640 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
27641 let result_id = Some(new_result_id.to_string());
27642 assert_eq!(
27643 params.text_document.uri,
27644 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
27645 );
27646 async move {
27647 Ok(lsp::DocumentDiagnosticReportResult::Report(
27648 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
27649 related_documents: None,
27650 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
27651 items: Vec::new(),
27652 result_id,
27653 },
27654 }),
27655 ))
27656 }
27657 });
27658
27659 let ensure_result_id = |expected_result_id: Option<SharedString>, cx: &mut TestAppContext| {
27660 project.update(cx, |project, cx| {
27661 let buffer_id = editor
27662 .read(cx)
27663 .buffer()
27664 .read(cx)
27665 .as_singleton()
27666 .expect("created a singleton buffer")
27667 .read(cx)
27668 .remote_id();
27669 let buffer_result_id = project
27670 .lsp_store()
27671 .read(cx)
27672 .result_id_for_buffer_pull(server_id, buffer_id, &None, cx);
27673 assert_eq!(expected_result_id, buffer_result_id);
27674 });
27675 };
27676
27677 ensure_result_id(None, cx);
27678 cx.executor().advance_clock(Duration::from_millis(60));
27679 cx.executor().run_until_parked();
27680 assert_eq!(
27681 diagnostic_requests.load(atomic::Ordering::Acquire),
27682 1,
27683 "Opening file should trigger diagnostic request"
27684 );
27685 first_request
27686 .next()
27687 .await
27688 .expect("should have sent the first diagnostics pull request");
27689 ensure_result_id(Some(SharedString::new_static("1")), cx);
27690
27691 // Editing should trigger diagnostics
27692 editor.update_in(cx, |editor, window, cx| {
27693 editor.handle_input("2", window, cx)
27694 });
27695 cx.executor().advance_clock(Duration::from_millis(60));
27696 cx.executor().run_until_parked();
27697 assert_eq!(
27698 diagnostic_requests.load(atomic::Ordering::Acquire),
27699 2,
27700 "Editing should trigger diagnostic request"
27701 );
27702 ensure_result_id(Some(SharedString::new_static("2")), cx);
27703
27704 // Moving cursor should not trigger diagnostic request
27705 editor.update_in(cx, |editor, window, cx| {
27706 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
27707 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
27708 });
27709 });
27710 cx.executor().advance_clock(Duration::from_millis(60));
27711 cx.executor().run_until_parked();
27712 assert_eq!(
27713 diagnostic_requests.load(atomic::Ordering::Acquire),
27714 2,
27715 "Cursor movement should not trigger diagnostic request"
27716 );
27717 ensure_result_id(Some(SharedString::new_static("2")), cx);
27718 // Multiple rapid edits should be debounced
27719 for _ in 0..5 {
27720 editor.update_in(cx, |editor, window, cx| {
27721 editor.handle_input("x", window, cx)
27722 });
27723 }
27724 cx.executor().advance_clock(Duration::from_millis(60));
27725 cx.executor().run_until_parked();
27726
27727 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
27728 assert!(
27729 final_requests <= 4,
27730 "Multiple rapid edits should be debounced (got {final_requests} requests)",
27731 );
27732 ensure_result_id(Some(SharedString::new(final_requests.to_string())), cx);
27733}
27734
27735#[gpui::test]
27736async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
27737 // Regression test for issue #11671
27738 // Previously, adding a cursor after moving multiple cursors would reset
27739 // the cursor count instead of adding to the existing cursors.
27740 init_test(cx, |_| {});
27741 let mut cx = EditorTestContext::new(cx).await;
27742
27743 // Create a simple buffer with cursor at start
27744 cx.set_state(indoc! {"
27745 ˇaaaa
27746 bbbb
27747 cccc
27748 dddd
27749 eeee
27750 ffff
27751 gggg
27752 hhhh"});
27753
27754 // Add 2 cursors below (so we have 3 total)
27755 cx.update_editor(|editor, window, cx| {
27756 editor.add_selection_below(&Default::default(), window, cx);
27757 editor.add_selection_below(&Default::default(), window, cx);
27758 });
27759
27760 // Verify we have 3 cursors
27761 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
27762 assert_eq!(
27763 initial_count, 3,
27764 "Should have 3 cursors after adding 2 below"
27765 );
27766
27767 // Move down one line
27768 cx.update_editor(|editor, window, cx| {
27769 editor.move_down(&MoveDown, window, cx);
27770 });
27771
27772 // Add another cursor below
27773 cx.update_editor(|editor, window, cx| {
27774 editor.add_selection_below(&Default::default(), window, cx);
27775 });
27776
27777 // Should now have 4 cursors (3 original + 1 new)
27778 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
27779 assert_eq!(
27780 final_count, 4,
27781 "Should have 4 cursors after moving and adding another"
27782 );
27783}
27784
27785#[gpui::test]
27786async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
27787 init_test(cx, |_| {});
27788
27789 let mut cx = EditorTestContext::new(cx).await;
27790
27791 cx.set_state(indoc!(
27792 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
27793 Second line here"#
27794 ));
27795
27796 cx.update_editor(|editor, window, cx| {
27797 // Enable soft wrapping with a narrow width to force soft wrapping and
27798 // confirm that more than 2 rows are being displayed.
27799 editor.set_wrap_width(Some(100.0.into()), cx);
27800 assert!(editor.display_text(cx).lines().count() > 2);
27801
27802 editor.add_selection_below(
27803 &AddSelectionBelow {
27804 skip_soft_wrap: true,
27805 },
27806 window,
27807 cx,
27808 );
27809
27810 assert_eq!(
27811 display_ranges(editor, cx),
27812 &[
27813 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
27814 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
27815 ]
27816 );
27817
27818 editor.add_selection_above(
27819 &AddSelectionAbove {
27820 skip_soft_wrap: true,
27821 },
27822 window,
27823 cx,
27824 );
27825
27826 assert_eq!(
27827 display_ranges(editor, cx),
27828 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27829 );
27830
27831 editor.add_selection_below(
27832 &AddSelectionBelow {
27833 skip_soft_wrap: false,
27834 },
27835 window,
27836 cx,
27837 );
27838
27839 assert_eq!(
27840 display_ranges(editor, cx),
27841 &[
27842 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
27843 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
27844 ]
27845 );
27846
27847 editor.add_selection_above(
27848 &AddSelectionAbove {
27849 skip_soft_wrap: false,
27850 },
27851 window,
27852 cx,
27853 );
27854
27855 assert_eq!(
27856 display_ranges(editor, cx),
27857 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
27858 );
27859 });
27860
27861 // Set up text where selections are in the middle of a soft-wrapped line.
27862 // When adding selection below with `skip_soft_wrap` set to `true`, the new
27863 // selection should be at the same buffer column, not the same pixel
27864 // position.
27865 cx.set_state(indoc!(
27866 r#"1. Very long line to show «howˇ» a wrapped line would look
27867 2. Very long line to show how a wrapped line would look"#
27868 ));
27869
27870 cx.update_editor(|editor, window, cx| {
27871 // Enable soft wrapping with a narrow width to force soft wrapping and
27872 // confirm that more than 2 rows are being displayed.
27873 editor.set_wrap_width(Some(100.0.into()), cx);
27874 assert!(editor.display_text(cx).lines().count() > 2);
27875
27876 editor.add_selection_below(
27877 &AddSelectionBelow {
27878 skip_soft_wrap: true,
27879 },
27880 window,
27881 cx,
27882 );
27883
27884 // Assert that there's now 2 selections, both selecting the same column
27885 // range in the buffer row.
27886 let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx));
27887 let selections = editor.selections.all::<Point>(&display_map);
27888 assert_eq!(selections.len(), 2);
27889 assert_eq!(selections[0].start.column, selections[1].start.column);
27890 assert_eq!(selections[0].end.column, selections[1].end.column);
27891 });
27892}
27893
27894#[gpui::test]
27895async fn test_insert_snippet(cx: &mut TestAppContext) {
27896 init_test(cx, |_| {});
27897 let mut cx = EditorTestContext::new(cx).await;
27898
27899 cx.update_editor(|editor, _, cx| {
27900 editor.project().unwrap().update(cx, |project, cx| {
27901 project.snippets().update(cx, |snippets, _cx| {
27902 let snippet = project::snippet_provider::Snippet {
27903 prefix: vec![], // no prefix needed!
27904 body: "an Unspecified".to_string(),
27905 description: Some("shhhh it's a secret".to_string()),
27906 name: "super secret snippet".to_string(),
27907 };
27908 snippets.add_snippet_for_test(
27909 None,
27910 PathBuf::from("test_snippets.json"),
27911 vec![Arc::new(snippet)],
27912 );
27913
27914 let snippet = project::snippet_provider::Snippet {
27915 prefix: vec![], // no prefix needed!
27916 body: " Location".to_string(),
27917 description: Some("the word 'location'".to_string()),
27918 name: "location word".to_string(),
27919 };
27920 snippets.add_snippet_for_test(
27921 Some("Markdown".to_string()),
27922 PathBuf::from("test_snippets.json"),
27923 vec![Arc::new(snippet)],
27924 );
27925 });
27926 })
27927 });
27928
27929 cx.set_state(indoc!(r#"First cursor at ˇ and second cursor at ˇ"#));
27930
27931 cx.update_editor(|editor, window, cx| {
27932 editor.insert_snippet_at_selections(
27933 &InsertSnippet {
27934 language: None,
27935 name: Some("super secret snippet".to_string()),
27936 snippet: None,
27937 },
27938 window,
27939 cx,
27940 );
27941
27942 // Language is specified in the action,
27943 // so the buffer language does not need to match
27944 editor.insert_snippet_at_selections(
27945 &InsertSnippet {
27946 language: Some("Markdown".to_string()),
27947 name: Some("location word".to_string()),
27948 snippet: None,
27949 },
27950 window,
27951 cx,
27952 );
27953
27954 editor.insert_snippet_at_selections(
27955 &InsertSnippet {
27956 language: None,
27957 name: None,
27958 snippet: Some("$0 after".to_string()),
27959 },
27960 window,
27961 cx,
27962 );
27963 });
27964
27965 cx.assert_editor_state(
27966 r#"First cursor at an Unspecified Locationˇ after and second cursor at an Unspecified Locationˇ after"#,
27967 );
27968}
27969
27970#[gpui::test]
27971async fn test_inlay_hints_request_timeout(cx: &mut TestAppContext) {
27972 use crate::inlays::inlay_hints::InlayHintRefreshReason;
27973 use crate::inlays::inlay_hints::tests::{cached_hint_labels, init_test, visible_hint_labels};
27974 use settings::InlayHintSettingsContent;
27975 use std::sync::atomic::AtomicU32;
27976 use std::time::Duration;
27977
27978 const BASE_TIMEOUT_SECS: u64 = 1;
27979
27980 let request_count = Arc::new(AtomicU32::new(0));
27981 let closure_request_count = request_count.clone();
27982
27983 init_test(cx, |settings| {
27984 settings.defaults.inlay_hints = Some(InlayHintSettingsContent {
27985 enabled: Some(true),
27986 ..InlayHintSettingsContent::default()
27987 })
27988 });
27989 cx.update(|cx| {
27990 SettingsStore::update_global(cx, |store, cx| {
27991 store.update_user_settings(cx, |settings| {
27992 settings.global_lsp_settings = Some(GlobalLspSettingsContent {
27993 request_timeout: Some(BASE_TIMEOUT_SECS),
27994 button: Some(true),
27995 notifications: None,
27996 semantic_token_rules: None,
27997 });
27998 });
27999 });
28000 });
28001
28002 let fs = FakeFs::new(cx.executor());
28003 fs.insert_tree(
28004 path!("/a"),
28005 json!({
28006 "main.rs": "fn main() { let a = 5; }",
28007 }),
28008 )
28009 .await;
28010
28011 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
28012 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
28013 language_registry.add(rust_lang());
28014 let mut fake_servers = language_registry.register_fake_lsp(
28015 "Rust",
28016 FakeLspAdapter {
28017 capabilities: lsp::ServerCapabilities {
28018 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
28019 ..lsp::ServerCapabilities::default()
28020 },
28021 initializer: Some(Box::new(move |fake_server| {
28022 let request_count = closure_request_count.clone();
28023 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
28024 move |params, cx| {
28025 let request_count = request_count.clone();
28026 async move {
28027 cx.background_executor()
28028 .timer(Duration::from_secs(BASE_TIMEOUT_SECS * 2))
28029 .await;
28030 let count = request_count.fetch_add(1, atomic::Ordering::Release) + 1;
28031 assert_eq!(
28032 params.text_document.uri,
28033 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
28034 );
28035 Ok(Some(vec![lsp::InlayHint {
28036 position: lsp::Position::new(0, 1),
28037 label: lsp::InlayHintLabel::String(count.to_string()),
28038 kind: None,
28039 text_edits: None,
28040 tooltip: None,
28041 padding_left: None,
28042 padding_right: None,
28043 data: None,
28044 }]))
28045 }
28046 },
28047 );
28048 })),
28049 ..FakeLspAdapter::default()
28050 },
28051 );
28052
28053 let buffer = project
28054 .update(cx, |project, cx| {
28055 project.open_local_buffer(path!("/a/main.rs"), cx)
28056 })
28057 .await
28058 .unwrap();
28059 let editor = cx.add_window(|window, cx| Editor::for_buffer(buffer, Some(project), window, cx));
28060
28061 cx.executor().run_until_parked();
28062 let fake_server = fake_servers.next().await.unwrap();
28063
28064 cx.executor()
28065 .advance_clock(Duration::from_secs(BASE_TIMEOUT_SECS) + Duration::from_millis(100));
28066 cx.executor().run_until_parked();
28067 editor
28068 .update(cx, |editor, _window, cx| {
28069 assert!(
28070 cached_hint_labels(editor, cx).is_empty(),
28071 "First request should time out, no hints cached"
28072 );
28073 })
28074 .unwrap();
28075
28076 editor
28077 .update(cx, |editor, _window, cx| {
28078 editor.refresh_inlay_hints(
28079 InlayHintRefreshReason::RefreshRequested {
28080 server_id: fake_server.server.server_id(),
28081 request_id: Some(1),
28082 },
28083 cx,
28084 );
28085 })
28086 .unwrap();
28087 cx.executor()
28088 .advance_clock(Duration::from_secs(BASE_TIMEOUT_SECS) + Duration::from_millis(100));
28089 cx.executor().run_until_parked();
28090 editor
28091 .update(cx, |editor, _window, cx| {
28092 assert!(
28093 cached_hint_labels(editor, cx).is_empty(),
28094 "Second request should also time out with BASE_TIMEOUT, no hints cached"
28095 );
28096 })
28097 .unwrap();
28098
28099 cx.update(|cx| {
28100 SettingsStore::update_global(cx, |store, cx| {
28101 store.update_user_settings(cx, |settings| {
28102 settings.global_lsp_settings = Some(GlobalLspSettingsContent {
28103 request_timeout: Some(BASE_TIMEOUT_SECS * 4),
28104 button: Some(true),
28105 notifications: None,
28106 semantic_token_rules: None,
28107 });
28108 });
28109 });
28110 });
28111 editor
28112 .update(cx, |editor, _window, cx| {
28113 editor.refresh_inlay_hints(
28114 InlayHintRefreshReason::RefreshRequested {
28115 server_id: fake_server.server.server_id(),
28116 request_id: Some(2),
28117 },
28118 cx,
28119 );
28120 })
28121 .unwrap();
28122 cx.executor()
28123 .advance_clock(Duration::from_secs(BASE_TIMEOUT_SECS * 4) + Duration::from_millis(100));
28124 cx.executor().run_until_parked();
28125 editor
28126 .update(cx, |editor, _window, cx| {
28127 assert_eq!(
28128 vec!["1".to_string()],
28129 cached_hint_labels(editor, cx),
28130 "With extended timeout (BASE * 4), hints should arrive successfully"
28131 );
28132 assert_eq!(vec!["1".to_string()], visible_hint_labels(editor, cx));
28133 })
28134 .unwrap();
28135}
28136
28137#[gpui::test]
28138async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
28139 init_test(cx, |_| {});
28140 let (editor, cx) = cx.add_window_view(Editor::single_line);
28141 editor.update_in(cx, |editor, window, cx| {
28142 editor.set_text("oops\n\nwow\n", window, cx)
28143 });
28144 cx.run_until_parked();
28145 editor.update(cx, |editor, cx| {
28146 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
28147 });
28148 editor.update(cx, |editor, cx| {
28149 editor.edit([(MultiBufferOffset(3)..MultiBufferOffset(5), "")], cx)
28150 });
28151 cx.run_until_parked();
28152 editor.update(cx, |editor, cx| {
28153 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
28154 });
28155}
28156
28157#[gpui::test]
28158async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
28159 init_test(cx, |_| {});
28160
28161 cx.update(|cx| {
28162 register_project_item::<Editor>(cx);
28163 });
28164
28165 let fs = FakeFs::new(cx.executor());
28166 fs.insert_tree("/root1", json!({})).await;
28167 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
28168 .await;
28169
28170 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
28171 let (workspace, cx) =
28172 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
28173
28174 let worktree_id = project.update(cx, |project, cx| {
28175 project.worktrees(cx).next().unwrap().read(cx).id()
28176 });
28177
28178 let handle = workspace
28179 .update_in(cx, |workspace, window, cx| {
28180 let project_path = (worktree_id, rel_path("one.pdf"));
28181 workspace.open_path(project_path, None, true, window, cx)
28182 })
28183 .await
28184 .unwrap();
28185 // The test file content `vec![0xff, 0xfe, ...]` starts with a UTF-16 LE BOM.
28186 // Previously, this fell back to `InvalidItemView` because it wasn't valid UTF-8.
28187 // With auto-detection enabled, this is now recognized as UTF-16 and opens in the Editor.
28188 assert_eq!(handle.to_any_view().entity_type(), TypeId::of::<Editor>());
28189}
28190
28191#[gpui::test]
28192async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
28193 init_test(cx, |_| {});
28194
28195 let language = Arc::new(Language::new(
28196 LanguageConfig::default(),
28197 Some(tree_sitter_rust::LANGUAGE.into()),
28198 ));
28199
28200 // Test hierarchical sibling navigation
28201 let text = r#"
28202 fn outer() {
28203 if condition {
28204 let a = 1;
28205 }
28206 let b = 2;
28207 }
28208
28209 fn another() {
28210 let c = 3;
28211 }
28212 "#;
28213
28214 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
28215 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
28216 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
28217
28218 // Wait for parsing to complete
28219 editor
28220 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
28221 .await;
28222
28223 editor.update_in(cx, |editor, window, cx| {
28224 // Start by selecting "let a = 1;" inside the if block
28225 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28226 s.select_display_ranges([
28227 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
28228 ]);
28229 });
28230
28231 let initial_selection = editor
28232 .selections
28233 .display_ranges(&editor.display_snapshot(cx));
28234 assert_eq!(initial_selection.len(), 1, "Should have one selection");
28235
28236 // Test select next sibling - should move up levels to find the next sibling
28237 // Since "let a = 1;" has no siblings in the if block, it should move up
28238 // to find "let b = 2;" which is a sibling of the if block
28239 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
28240 let next_selection = editor
28241 .selections
28242 .display_ranges(&editor.display_snapshot(cx));
28243
28244 // Should have a selection and it should be different from the initial
28245 assert_eq!(
28246 next_selection.len(),
28247 1,
28248 "Should have one selection after next"
28249 );
28250 assert_ne!(
28251 next_selection[0], initial_selection[0],
28252 "Next sibling selection should be different"
28253 );
28254
28255 // Test hierarchical navigation by going to the end of the current function
28256 // and trying to navigate to the next function
28257 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28258 s.select_display_ranges([
28259 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
28260 ]);
28261 });
28262
28263 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
28264 let function_next_selection = editor
28265 .selections
28266 .display_ranges(&editor.display_snapshot(cx));
28267
28268 // Should move to the next function
28269 assert_eq!(
28270 function_next_selection.len(),
28271 1,
28272 "Should have one selection after function next"
28273 );
28274
28275 // Test select previous sibling navigation
28276 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
28277 let prev_selection = editor
28278 .selections
28279 .display_ranges(&editor.display_snapshot(cx));
28280
28281 // Should have a selection and it should be different
28282 assert_eq!(
28283 prev_selection.len(),
28284 1,
28285 "Should have one selection after prev"
28286 );
28287 assert_ne!(
28288 prev_selection[0], function_next_selection[0],
28289 "Previous sibling selection should be different from next"
28290 );
28291 });
28292}
28293
28294#[gpui::test]
28295async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
28296 init_test(cx, |_| {});
28297
28298 let mut cx = EditorTestContext::new(cx).await;
28299 cx.set_state(
28300 "let ˇvariable = 42;
28301let another = variable + 1;
28302let result = variable * 2;",
28303 );
28304
28305 // Set up document highlights manually (simulating LSP response)
28306 cx.update_editor(|editor, _window, cx| {
28307 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
28308
28309 // Create highlights for "variable" occurrences
28310 let highlight_ranges = [
28311 Point::new(0, 4)..Point::new(0, 12), // First "variable"
28312 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
28313 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
28314 ];
28315
28316 let anchor_ranges: Vec<_> = highlight_ranges
28317 .iter()
28318 .map(|range| range.clone().to_anchors(&buffer_snapshot))
28319 .collect();
28320
28321 editor.highlight_background(
28322 HighlightKey::DocumentHighlightRead,
28323 &anchor_ranges,
28324 |_, theme| theme.colors().editor_document_highlight_read_background,
28325 cx,
28326 );
28327 });
28328
28329 // Go to next highlight - should move to second "variable"
28330 cx.update_editor(|editor, window, cx| {
28331 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
28332 });
28333 cx.assert_editor_state(
28334 "let variable = 42;
28335let another = ˇvariable + 1;
28336let result = variable * 2;",
28337 );
28338
28339 // Go to next highlight - should move to third "variable"
28340 cx.update_editor(|editor, window, cx| {
28341 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
28342 });
28343 cx.assert_editor_state(
28344 "let variable = 42;
28345let another = variable + 1;
28346let result = ˇvariable * 2;",
28347 );
28348
28349 // Go to next highlight - should stay at third "variable" (no wrap-around)
28350 cx.update_editor(|editor, window, cx| {
28351 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
28352 });
28353 cx.assert_editor_state(
28354 "let variable = 42;
28355let another = variable + 1;
28356let result = ˇvariable * 2;",
28357 );
28358
28359 // Now test going backwards from third position
28360 cx.update_editor(|editor, window, cx| {
28361 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
28362 });
28363 cx.assert_editor_state(
28364 "let variable = 42;
28365let another = ˇvariable + 1;
28366let result = variable * 2;",
28367 );
28368
28369 // Go to previous highlight - should move to first "variable"
28370 cx.update_editor(|editor, window, cx| {
28371 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
28372 });
28373 cx.assert_editor_state(
28374 "let ˇvariable = 42;
28375let another = variable + 1;
28376let result = variable * 2;",
28377 );
28378
28379 // Go to previous highlight - should stay on first "variable"
28380 cx.update_editor(|editor, window, cx| {
28381 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
28382 });
28383 cx.assert_editor_state(
28384 "let ˇvariable = 42;
28385let another = variable + 1;
28386let result = variable * 2;",
28387 );
28388}
28389
28390#[gpui::test]
28391async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
28392 cx: &mut gpui::TestAppContext,
28393) {
28394 init_test(cx, |_| {});
28395
28396 let url = "https://zed.dev";
28397
28398 let markdown_language = Arc::new(Language::new(
28399 LanguageConfig {
28400 name: "Markdown".into(),
28401 ..LanguageConfig::default()
28402 },
28403 None,
28404 ));
28405
28406 let mut cx = EditorTestContext::new(cx).await;
28407 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28408 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
28409
28410 cx.update_editor(|editor, window, cx| {
28411 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28412 editor.paste(&Paste, window, cx);
28413 });
28414
28415 cx.assert_editor_state(&format!(
28416 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
28417 ));
28418}
28419
28420#[gpui::test]
28421async fn test_markdown_indents(cx: &mut gpui::TestAppContext) {
28422 init_test(cx, |_| {});
28423
28424 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
28425 let mut cx = EditorTestContext::new(cx).await;
28426
28427 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28428
28429 // Case 1: Test if adding a character with multi cursors preserves nested list indents
28430 cx.set_state(&indoc! {"
28431 - [ ] Item 1
28432 - [ ] Item 1.a
28433 - [ˇ] Item 2
28434 - [ˇ] Item 2.a
28435 - [ˇ] Item 2.b
28436 "
28437 });
28438 cx.update_editor(|editor, window, cx| {
28439 editor.handle_input("x", window, cx);
28440 });
28441 cx.run_until_parked();
28442 cx.assert_editor_state(indoc! {"
28443 - [ ] Item 1
28444 - [ ] Item 1.a
28445 - [xˇ] Item 2
28446 - [xˇ] Item 2.a
28447 - [xˇ] Item 2.b
28448 "
28449 });
28450
28451 // Case 2: Test adding new line after nested list continues the list with unchecked task
28452 cx.set_state(&indoc! {"
28453 - [ ] Item 1
28454 - [ ] Item 1.a
28455 - [x] Item 2
28456 - [x] Item 2.a
28457 - [x] Item 2.bˇ"
28458 });
28459 cx.update_editor(|editor, window, cx| {
28460 editor.newline(&Newline, window, cx);
28461 });
28462 cx.assert_editor_state(indoc! {"
28463 - [ ] Item 1
28464 - [ ] Item 1.a
28465 - [x] Item 2
28466 - [x] Item 2.a
28467 - [x] Item 2.b
28468 - [ ] ˇ"
28469 });
28470
28471 // Case 3: Test adding content to continued list item
28472 cx.update_editor(|editor, window, cx| {
28473 editor.handle_input("Item 2.c", window, cx);
28474 });
28475 cx.run_until_parked();
28476 cx.assert_editor_state(indoc! {"
28477 - [ ] Item 1
28478 - [ ] Item 1.a
28479 - [x] Item 2
28480 - [x] Item 2.a
28481 - [x] Item 2.b
28482 - [ ] Item 2.cˇ"
28483 });
28484
28485 // Case 4: Test adding new line after nested ordered list continues with next number
28486 cx.set_state(indoc! {"
28487 1. Item 1
28488 1. Item 1.a
28489 2. Item 2
28490 1. Item 2.a
28491 2. Item 2.bˇ"
28492 });
28493 cx.update_editor(|editor, window, cx| {
28494 editor.newline(&Newline, window, cx);
28495 });
28496 cx.assert_editor_state(indoc! {"
28497 1. Item 1
28498 1. Item 1.a
28499 2. Item 2
28500 1. Item 2.a
28501 2. Item 2.b
28502 3. ˇ"
28503 });
28504
28505 // Case 5: Adding content to continued ordered list item
28506 cx.update_editor(|editor, window, cx| {
28507 editor.handle_input("Item 2.c", window, cx);
28508 });
28509 cx.run_until_parked();
28510 cx.assert_editor_state(indoc! {"
28511 1. Item 1
28512 1. Item 1.a
28513 2. Item 2
28514 1. Item 2.a
28515 2. Item 2.b
28516 3. Item 2.cˇ"
28517 });
28518
28519 // Case 6: Test adding new line after nested ordered list preserves indent of previous line
28520 cx.set_state(indoc! {"
28521 - Item 1
28522 - Item 1.a
28523 - Item 1.a
28524 ˇ"});
28525 cx.update_editor(|editor, window, cx| {
28526 editor.handle_input("-", window, cx);
28527 });
28528 cx.run_until_parked();
28529 cx.assert_editor_state(indoc! {"
28530 - Item 1
28531 - Item 1.a
28532 - Item 1.a
28533 -ˇ"});
28534
28535 // Case 7: Test blockquote newline preserves something
28536 cx.set_state(indoc! {"
28537 > Item 1ˇ"
28538 });
28539 cx.update_editor(|editor, window, cx| {
28540 editor.newline(&Newline, window, cx);
28541 });
28542 cx.assert_editor_state(indoc! {"
28543 > Item 1
28544 ˇ"
28545 });
28546}
28547
28548#[gpui::test]
28549async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
28550 cx: &mut gpui::TestAppContext,
28551) {
28552 init_test(cx, |_| {});
28553
28554 let url = "https://zed.dev";
28555
28556 let markdown_language = Arc::new(Language::new(
28557 LanguageConfig {
28558 name: "Markdown".into(),
28559 ..LanguageConfig::default()
28560 },
28561 None,
28562 ));
28563
28564 let mut cx = EditorTestContext::new(cx).await;
28565 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28566 cx.set_state(&format!(
28567 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
28568 ));
28569
28570 cx.update_editor(|editor, window, cx| {
28571 editor.copy(&Copy, window, cx);
28572 });
28573
28574 cx.set_state(&format!(
28575 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
28576 ));
28577
28578 cx.update_editor(|editor, window, cx| {
28579 editor.paste(&Paste, window, cx);
28580 });
28581
28582 cx.assert_editor_state(&format!(
28583 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
28584 ));
28585}
28586
28587#[gpui::test]
28588async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
28589 cx: &mut gpui::TestAppContext,
28590) {
28591 init_test(cx, |_| {});
28592
28593 let url = "https://zed.dev";
28594
28595 let markdown_language = Arc::new(Language::new(
28596 LanguageConfig {
28597 name: "Markdown".into(),
28598 ..LanguageConfig::default()
28599 },
28600 None,
28601 ));
28602
28603 let mut cx = EditorTestContext::new(cx).await;
28604 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28605 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
28606
28607 cx.update_editor(|editor, window, cx| {
28608 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28609 editor.paste(&Paste, window, cx);
28610 });
28611
28612 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
28613}
28614
28615#[gpui::test]
28616async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
28617 cx: &mut gpui::TestAppContext,
28618) {
28619 init_test(cx, |_| {});
28620
28621 let text = "Awesome";
28622
28623 let markdown_language = Arc::new(Language::new(
28624 LanguageConfig {
28625 name: "Markdown".into(),
28626 ..LanguageConfig::default()
28627 },
28628 None,
28629 ));
28630
28631 let mut cx = EditorTestContext::new(cx).await;
28632 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28633 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
28634
28635 cx.update_editor(|editor, window, cx| {
28636 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
28637 editor.paste(&Paste, window, cx);
28638 });
28639
28640 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
28641}
28642
28643#[gpui::test]
28644async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
28645 cx: &mut gpui::TestAppContext,
28646) {
28647 init_test(cx, |_| {});
28648
28649 let url = "https://zed.dev";
28650
28651 let markdown_language = Arc::new(Language::new(
28652 LanguageConfig {
28653 name: "Rust".into(),
28654 ..LanguageConfig::default()
28655 },
28656 None,
28657 ));
28658
28659 let mut cx = EditorTestContext::new(cx).await;
28660 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
28661 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
28662
28663 cx.update_editor(|editor, window, cx| {
28664 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28665 editor.paste(&Paste, window, cx);
28666 });
28667
28668 cx.assert_editor_state(&format!(
28669 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
28670 ));
28671}
28672
28673#[gpui::test]
28674async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
28675 cx: &mut TestAppContext,
28676) {
28677 init_test(cx, |_| {});
28678
28679 let url = "https://zed.dev";
28680
28681 let markdown_language = Arc::new(Language::new(
28682 LanguageConfig {
28683 name: "Markdown".into(),
28684 ..LanguageConfig::default()
28685 },
28686 None,
28687 ));
28688
28689 let (editor, cx) = cx.add_window_view(|window, cx| {
28690 let multi_buffer = MultiBuffer::build_multi(
28691 [
28692 ("this will embed -> link", vec![Point::row_range(0..1)]),
28693 ("this will replace -> link", vec![Point::row_range(0..1)]),
28694 ],
28695 cx,
28696 );
28697 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
28698 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28699 s.select_ranges(vec![
28700 Point::new(0, 19)..Point::new(0, 23),
28701 Point::new(1, 21)..Point::new(1, 25),
28702 ])
28703 });
28704 let first_buffer_id = multi_buffer
28705 .read(cx)
28706 .excerpt_buffer_ids()
28707 .into_iter()
28708 .next()
28709 .unwrap();
28710 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
28711 first_buffer.update(cx, |buffer, cx| {
28712 buffer.set_language(Some(markdown_language.clone()), cx);
28713 });
28714
28715 editor
28716 });
28717 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
28718
28719 cx.update_editor(|editor, window, cx| {
28720 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
28721 editor.paste(&Paste, window, cx);
28722 });
28723
28724 cx.assert_editor_state(&format!(
28725 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
28726 ));
28727}
28728
28729#[gpui::test]
28730async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
28731 init_test(cx, |_| {});
28732
28733 let fs = FakeFs::new(cx.executor());
28734 fs.insert_tree(
28735 path!("/project"),
28736 json!({
28737 "first.rs": "# First Document\nSome content here.",
28738 "second.rs": "Plain text content for second file.",
28739 }),
28740 )
28741 .await;
28742
28743 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
28744 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
28745 let cx = &mut VisualTestContext::from_window(*workspace, cx);
28746
28747 let language = rust_lang();
28748 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
28749 language_registry.add(language.clone());
28750 let mut fake_servers = language_registry.register_fake_lsp(
28751 "Rust",
28752 FakeLspAdapter {
28753 ..FakeLspAdapter::default()
28754 },
28755 );
28756
28757 let buffer1 = project
28758 .update(cx, |project, cx| {
28759 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
28760 })
28761 .await
28762 .unwrap();
28763 let buffer2 = project
28764 .update(cx, |project, cx| {
28765 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
28766 })
28767 .await
28768 .unwrap();
28769
28770 let multi_buffer = cx.new(|cx| {
28771 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
28772 multi_buffer.set_excerpts_for_path(
28773 PathKey::for_buffer(&buffer1, cx),
28774 buffer1.clone(),
28775 [Point::zero()..buffer1.read(cx).max_point()],
28776 3,
28777 cx,
28778 );
28779 multi_buffer.set_excerpts_for_path(
28780 PathKey::for_buffer(&buffer2, cx),
28781 buffer2.clone(),
28782 [Point::zero()..buffer1.read(cx).max_point()],
28783 3,
28784 cx,
28785 );
28786 multi_buffer
28787 });
28788
28789 let (editor, cx) = cx.add_window_view(|window, cx| {
28790 Editor::new(
28791 EditorMode::full(),
28792 multi_buffer,
28793 Some(project.clone()),
28794 window,
28795 cx,
28796 )
28797 });
28798
28799 let fake_language_server = fake_servers.next().await.unwrap();
28800
28801 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
28802
28803 let save = editor.update_in(cx, |editor, window, cx| {
28804 assert!(editor.is_dirty(cx));
28805
28806 editor.save(
28807 SaveOptions {
28808 format: true,
28809 autosave: true,
28810 },
28811 project,
28812 window,
28813 cx,
28814 )
28815 });
28816 let (start_edit_tx, start_edit_rx) = oneshot::channel();
28817 let (done_edit_tx, done_edit_rx) = oneshot::channel();
28818 let mut done_edit_rx = Some(done_edit_rx);
28819 let mut start_edit_tx = Some(start_edit_tx);
28820
28821 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
28822 start_edit_tx.take().unwrap().send(()).unwrap();
28823 let done_edit_rx = done_edit_rx.take().unwrap();
28824 async move {
28825 done_edit_rx.await.unwrap();
28826 Ok(None)
28827 }
28828 });
28829
28830 start_edit_rx.await.unwrap();
28831 buffer2
28832 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
28833 .unwrap();
28834
28835 done_edit_tx.send(()).unwrap();
28836
28837 save.await.unwrap();
28838 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
28839}
28840
28841#[gpui::test]
28842fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
28843 init_test(cx, |_| {});
28844
28845 let editor = cx.add_window(|window, cx| {
28846 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
28847 build_editor(buffer, window, cx)
28848 });
28849
28850 editor
28851 .update(cx, |editor, window, cx| {
28852 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
28853 s.select_display_ranges([
28854 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
28855 ])
28856 });
28857
28858 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
28859
28860 assert_eq!(
28861 editor.display_text(cx),
28862 "line1\nline2\nline2",
28863 "Duplicating last line upward should create duplicate above, not on same line"
28864 );
28865
28866 assert_eq!(
28867 editor
28868 .selections
28869 .display_ranges(&editor.display_snapshot(cx)),
28870 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
28871 "Selection should move to the duplicated line"
28872 );
28873 })
28874 .unwrap();
28875}
28876
28877#[gpui::test]
28878async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
28879 init_test(cx, |_| {});
28880
28881 let mut cx = EditorTestContext::new(cx).await;
28882
28883 cx.set_state("line1\nline2ˇ");
28884
28885 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28886
28887 let clipboard_text = cx
28888 .read_from_clipboard()
28889 .and_then(|item| item.text().as_deref().map(str::to_string));
28890
28891 assert_eq!(
28892 clipboard_text,
28893 Some("line2\n".to_string()),
28894 "Copying a line without trailing newline should include a newline"
28895 );
28896
28897 cx.set_state("line1\nˇ");
28898
28899 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28900
28901 cx.assert_editor_state("line1\nline2\nˇ");
28902}
28903
28904#[gpui::test]
28905async fn test_multi_selection_copy_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28906 init_test(cx, |_| {});
28907
28908 let mut cx = EditorTestContext::new(cx).await;
28909
28910 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28911
28912 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
28913
28914 let clipboard_text = cx
28915 .read_from_clipboard()
28916 .and_then(|item| item.text().as_deref().map(str::to_string));
28917
28918 assert_eq!(
28919 clipboard_text,
28920 Some("line1\nline2\nline3\n".to_string()),
28921 "Copying multiple lines should include a single newline between lines"
28922 );
28923
28924 cx.set_state("lineA\nˇ");
28925
28926 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28927
28928 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28929}
28930
28931#[gpui::test]
28932async fn test_multi_selection_cut_with_newline_between_copied_lines(cx: &mut TestAppContext) {
28933 init_test(cx, |_| {});
28934
28935 let mut cx = EditorTestContext::new(cx).await;
28936
28937 cx.set_state("ˇline1\nˇline2\nˇline3\n");
28938
28939 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
28940
28941 let clipboard_text = cx
28942 .read_from_clipboard()
28943 .and_then(|item| item.text().as_deref().map(str::to_string));
28944
28945 assert_eq!(
28946 clipboard_text,
28947 Some("line1\nline2\nline3\n".to_string()),
28948 "Copying multiple lines should include a single newline between lines"
28949 );
28950
28951 cx.set_state("lineA\nˇ");
28952
28953 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
28954
28955 cx.assert_editor_state("lineA\nline1\nline2\nline3\nˇ");
28956}
28957
28958#[gpui::test]
28959async fn test_end_of_editor_context(cx: &mut TestAppContext) {
28960 init_test(cx, |_| {});
28961
28962 let mut cx = EditorTestContext::new(cx).await;
28963
28964 cx.set_state("line1\nline2ˇ");
28965 cx.update_editor(|e, window, cx| {
28966 e.set_mode(EditorMode::SingleLine);
28967 assert!(e.key_context(window, cx).contains("end_of_input"));
28968 });
28969 cx.set_state("ˇline1\nline2");
28970 cx.update_editor(|e, window, cx| {
28971 assert!(!e.key_context(window, cx).contains("end_of_input"));
28972 });
28973 cx.set_state("line1ˇ\nline2");
28974 cx.update_editor(|e, window, cx| {
28975 assert!(!e.key_context(window, cx).contains("end_of_input"));
28976 });
28977}
28978
28979#[gpui::test]
28980async fn test_sticky_scroll(cx: &mut TestAppContext) {
28981 init_test(cx, |_| {});
28982 let mut cx = EditorTestContext::new(cx).await;
28983
28984 let buffer = indoc! {"
28985 ˇfn foo() {
28986 let abc = 123;
28987 }
28988 struct Bar;
28989 impl Bar {
28990 fn new() -> Self {
28991 Self
28992 }
28993 }
28994 fn baz() {
28995 }
28996 "};
28997 cx.set_state(&buffer);
28998
28999 cx.update_editor(|e, _, cx| {
29000 e.buffer()
29001 .read(cx)
29002 .as_singleton()
29003 .unwrap()
29004 .update(cx, |buffer, cx| {
29005 buffer.set_language(Some(rust_lang()), cx);
29006 })
29007 });
29008
29009 let mut sticky_headers = |offset: ScrollOffset| {
29010 cx.update_editor(|e, window, cx| {
29011 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
29012 });
29013 cx.run_until_parked();
29014 cx.update_editor(|e, window, cx| {
29015 EditorElement::sticky_headers(&e, &e.snapshot(window, cx))
29016 .into_iter()
29017 .map(
29018 |StickyHeader {
29019 start_point,
29020 offset,
29021 ..
29022 }| { (start_point, offset) },
29023 )
29024 .collect::<Vec<_>>()
29025 })
29026 };
29027
29028 let fn_foo = Point { row: 0, column: 0 };
29029 let impl_bar = Point { row: 4, column: 0 };
29030 let fn_new = Point { row: 5, column: 4 };
29031
29032 assert_eq!(sticky_headers(0.0), vec![]);
29033 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
29034 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
29035 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
29036 assert_eq!(sticky_headers(2.0), vec![]);
29037 assert_eq!(sticky_headers(2.5), vec![]);
29038 assert_eq!(sticky_headers(3.0), vec![]);
29039 assert_eq!(sticky_headers(3.5), vec![]);
29040 assert_eq!(sticky_headers(4.0), vec![]);
29041 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
29042 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
29043 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
29044 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
29045 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
29046 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
29047 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
29048 assert_eq!(sticky_headers(8.0), vec![]);
29049 assert_eq!(sticky_headers(8.5), vec![]);
29050 assert_eq!(sticky_headers(9.0), vec![]);
29051 assert_eq!(sticky_headers(9.5), vec![]);
29052 assert_eq!(sticky_headers(10.0), vec![]);
29053}
29054
29055#[gpui::test]
29056fn test_relative_line_numbers(cx: &mut TestAppContext) {
29057 init_test(cx, |_| {});
29058
29059 let buffer_1 = cx.new(|cx| Buffer::local("aaaaaaaaaa\nbbb\n", cx));
29060 let buffer_2 = cx.new(|cx| Buffer::local("cccccccccc\nddd\n", cx));
29061 let buffer_3 = cx.new(|cx| Buffer::local("eee\nffffffffff\n", cx));
29062
29063 let multibuffer = cx.new(|cx| {
29064 let mut multibuffer = MultiBuffer::new(ReadWrite);
29065 multibuffer.push_excerpts(
29066 buffer_1.clone(),
29067 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
29068 cx,
29069 );
29070 multibuffer.push_excerpts(
29071 buffer_2.clone(),
29072 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
29073 cx,
29074 );
29075 multibuffer.push_excerpts(
29076 buffer_3.clone(),
29077 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
29078 cx,
29079 );
29080 multibuffer
29081 });
29082
29083 // wrapped contents of multibuffer:
29084 // aaa
29085 // aaa
29086 // aaa
29087 // a
29088 // bbb
29089 //
29090 // ccc
29091 // ccc
29092 // ccc
29093 // c
29094 // ddd
29095 //
29096 // eee
29097 // fff
29098 // fff
29099 // fff
29100 // f
29101
29102 let editor = cx.add_window(|window, cx| build_editor(multibuffer, window, cx));
29103 _ = editor.update(cx, |editor, window, cx| {
29104 editor.set_wrap_width(Some(30.0.into()), cx); // every 3 characters
29105
29106 // includes trailing newlines.
29107 let expected_line_numbers = [2, 6, 7, 10, 14, 15, 18, 19, 23];
29108 let expected_wrapped_line_numbers = [
29109 2, 3, 4, 5, 6, 7, 10, 11, 12, 13, 14, 15, 18, 19, 20, 21, 22, 23,
29110 ];
29111
29112 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
29113 s.select_ranges([
29114 Point::new(7, 0)..Point::new(7, 1), // second row of `ccc`
29115 ]);
29116 });
29117
29118 let snapshot = editor.snapshot(window, cx);
29119
29120 // these are all 0-indexed
29121 let base_display_row = DisplayRow(11);
29122 let base_row = 3;
29123 let wrapped_base_row = 7;
29124
29125 // test not counting wrapped lines
29126 let expected_relative_numbers = expected_line_numbers
29127 .into_iter()
29128 .enumerate()
29129 .map(|(i, row)| (DisplayRow(row), i.abs_diff(base_row) as u32))
29130 .filter(|(_, relative_line_number)| *relative_line_number != 0)
29131 .collect_vec();
29132 let actual_relative_numbers = snapshot
29133 .calculate_relative_line_numbers(
29134 &(DisplayRow(0)..DisplayRow(24)),
29135 base_display_row,
29136 false,
29137 )
29138 .into_iter()
29139 .sorted()
29140 .collect_vec();
29141 assert_eq!(expected_relative_numbers, actual_relative_numbers);
29142 // check `calculate_relative_line_numbers()` against `relative_line_delta()` for each line
29143 for (display_row, relative_number) in expected_relative_numbers {
29144 assert_eq!(
29145 relative_number,
29146 snapshot
29147 .relative_line_delta(display_row, base_display_row, false)
29148 .unsigned_abs() as u32,
29149 );
29150 }
29151
29152 // test counting wrapped lines
29153 let expected_wrapped_relative_numbers = expected_wrapped_line_numbers
29154 .into_iter()
29155 .enumerate()
29156 .map(|(i, row)| (DisplayRow(row), i.abs_diff(wrapped_base_row) as u32))
29157 .filter(|(row, _)| *row != base_display_row)
29158 .collect_vec();
29159 let actual_relative_numbers = snapshot
29160 .calculate_relative_line_numbers(
29161 &(DisplayRow(0)..DisplayRow(24)),
29162 base_display_row,
29163 true,
29164 )
29165 .into_iter()
29166 .sorted()
29167 .collect_vec();
29168 assert_eq!(expected_wrapped_relative_numbers, actual_relative_numbers);
29169 // check `calculate_relative_line_numbers()` against `relative_wrapped_line_delta()` for each line
29170 for (display_row, relative_number) in expected_wrapped_relative_numbers {
29171 assert_eq!(
29172 relative_number,
29173 snapshot
29174 .relative_line_delta(display_row, base_display_row, true)
29175 .unsigned_abs() as u32,
29176 );
29177 }
29178 });
29179}
29180
29181#[gpui::test]
29182async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
29183 init_test(cx, |_| {});
29184 cx.update(|cx| {
29185 SettingsStore::update_global(cx, |store, cx| {
29186 store.update_user_settings(cx, |settings| {
29187 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
29188 enabled: Some(true),
29189 })
29190 });
29191 });
29192 });
29193 let mut cx = EditorTestContext::new(cx).await;
29194
29195 let line_height = cx.update_editor(|editor, window, cx| {
29196 editor
29197 .style(cx)
29198 .text
29199 .line_height_in_pixels(window.rem_size())
29200 });
29201
29202 let buffer = indoc! {"
29203 ˇfn foo() {
29204 let abc = 123;
29205 }
29206 struct Bar;
29207 impl Bar {
29208 fn new() -> Self {
29209 Self
29210 }
29211 }
29212 fn baz() {
29213 }
29214 "};
29215 cx.set_state(&buffer);
29216
29217 cx.update_editor(|e, _, cx| {
29218 e.buffer()
29219 .read(cx)
29220 .as_singleton()
29221 .unwrap()
29222 .update(cx, |buffer, cx| {
29223 buffer.set_language(Some(rust_lang()), cx);
29224 })
29225 });
29226
29227 let fn_foo = || empty_range(0, 0);
29228 let impl_bar = || empty_range(4, 0);
29229 let fn_new = || empty_range(5, 4);
29230
29231 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
29232 cx.update_editor(|e, window, cx| {
29233 e.scroll(
29234 gpui::Point {
29235 x: 0.,
29236 y: scroll_offset,
29237 },
29238 None,
29239 window,
29240 cx,
29241 );
29242 });
29243 cx.run_until_parked();
29244 cx.simulate_click(
29245 gpui::Point {
29246 x: px(0.),
29247 y: click_offset as f32 * line_height,
29248 },
29249 Modifiers::none(),
29250 );
29251 cx.run_until_parked();
29252 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
29253 };
29254 assert_eq!(
29255 scroll_and_click(
29256 4.5, // impl Bar is halfway off the screen
29257 0.0 // click top of screen
29258 ),
29259 // scrolled to impl Bar
29260 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
29261 );
29262
29263 assert_eq!(
29264 scroll_and_click(
29265 4.5, // impl Bar is halfway off the screen
29266 0.25 // click middle of impl Bar
29267 ),
29268 // scrolled to impl Bar
29269 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
29270 );
29271
29272 assert_eq!(
29273 scroll_and_click(
29274 4.5, // impl Bar is halfway off the screen
29275 1.5 // click below impl Bar (e.g. fn new())
29276 ),
29277 // scrolled to fn new() - this is below the impl Bar header which has persisted
29278 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
29279 );
29280
29281 assert_eq!(
29282 scroll_and_click(
29283 5.5, // fn new is halfway underneath impl Bar
29284 0.75 // click on the overlap of impl Bar and fn new()
29285 ),
29286 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
29287 );
29288
29289 assert_eq!(
29290 scroll_and_click(
29291 5.5, // fn new is halfway underneath impl Bar
29292 1.25 // click on the visible part of fn new()
29293 ),
29294 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
29295 );
29296
29297 assert_eq!(
29298 scroll_and_click(
29299 1.5, // fn foo is halfway off the screen
29300 0.0 // click top of screen
29301 ),
29302 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
29303 );
29304
29305 assert_eq!(
29306 scroll_and_click(
29307 1.5, // fn foo is halfway off the screen
29308 0.75 // click visible part of let abc...
29309 )
29310 .0,
29311 // no change in scroll
29312 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
29313 (gpui::Point { x: 0., y: 1.5 })
29314 );
29315}
29316
29317#[gpui::test]
29318async fn test_next_prev_reference(cx: &mut TestAppContext) {
29319 const CYCLE_POSITIONS: &[&'static str] = &[
29320 indoc! {"
29321 fn foo() {
29322 let ˇabc = 123;
29323 let x = abc + 1;
29324 let y = abc + 2;
29325 let z = abc + 2;
29326 }
29327 "},
29328 indoc! {"
29329 fn foo() {
29330 let abc = 123;
29331 let x = ˇabc + 1;
29332 let y = abc + 2;
29333 let z = abc + 2;
29334 }
29335 "},
29336 indoc! {"
29337 fn foo() {
29338 let abc = 123;
29339 let x = abc + 1;
29340 let y = ˇabc + 2;
29341 let z = abc + 2;
29342 }
29343 "},
29344 indoc! {"
29345 fn foo() {
29346 let abc = 123;
29347 let x = abc + 1;
29348 let y = abc + 2;
29349 let z = ˇabc + 2;
29350 }
29351 "},
29352 ];
29353
29354 init_test(cx, |_| {});
29355
29356 let mut cx = EditorLspTestContext::new_rust(
29357 lsp::ServerCapabilities {
29358 references_provider: Some(lsp::OneOf::Left(true)),
29359 ..Default::default()
29360 },
29361 cx,
29362 )
29363 .await;
29364
29365 // importantly, the cursor is in the middle
29366 cx.set_state(indoc! {"
29367 fn foo() {
29368 let aˇbc = 123;
29369 let x = abc + 1;
29370 let y = abc + 2;
29371 let z = abc + 2;
29372 }
29373 "});
29374
29375 let reference_ranges = [
29376 lsp::Position::new(1, 8),
29377 lsp::Position::new(2, 12),
29378 lsp::Position::new(3, 12),
29379 lsp::Position::new(4, 12),
29380 ]
29381 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
29382
29383 cx.lsp
29384 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
29385 Ok(Some(
29386 reference_ranges
29387 .map(|range| lsp::Location {
29388 uri: params.text_document_position.text_document.uri.clone(),
29389 range,
29390 })
29391 .to_vec(),
29392 ))
29393 });
29394
29395 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
29396 cx.update_editor(|editor, window, cx| {
29397 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
29398 })
29399 .unwrap()
29400 .await
29401 .unwrap()
29402 };
29403
29404 _move(Direction::Next, 1, &mut cx).await;
29405 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29406
29407 _move(Direction::Next, 1, &mut cx).await;
29408 cx.assert_editor_state(CYCLE_POSITIONS[2]);
29409
29410 _move(Direction::Next, 1, &mut cx).await;
29411 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29412
29413 // loops back to the start
29414 _move(Direction::Next, 1, &mut cx).await;
29415 cx.assert_editor_state(CYCLE_POSITIONS[0]);
29416
29417 // loops back to the end
29418 _move(Direction::Prev, 1, &mut cx).await;
29419 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29420
29421 _move(Direction::Prev, 1, &mut cx).await;
29422 cx.assert_editor_state(CYCLE_POSITIONS[2]);
29423
29424 _move(Direction::Prev, 1, &mut cx).await;
29425 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29426
29427 _move(Direction::Prev, 1, &mut cx).await;
29428 cx.assert_editor_state(CYCLE_POSITIONS[0]);
29429
29430 _move(Direction::Next, 3, &mut cx).await;
29431 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29432
29433 _move(Direction::Prev, 2, &mut cx).await;
29434 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29435}
29436
29437#[gpui::test]
29438async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
29439 init_test(cx, |_| {});
29440
29441 let (editor, cx) = cx.add_window_view(|window, cx| {
29442 let multi_buffer = MultiBuffer::build_multi(
29443 [
29444 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29445 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29446 ],
29447 cx,
29448 );
29449 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
29450 });
29451
29452 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29453 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
29454
29455 cx.assert_excerpts_with_selections(indoc! {"
29456 [EXCERPT]
29457 ˇ1
29458 2
29459 3
29460 [EXCERPT]
29461 1
29462 2
29463 3
29464 "});
29465
29466 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
29467 cx.update_editor(|editor, window, cx| {
29468 editor.change_selections(None.into(), window, cx, |s| {
29469 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29470 });
29471 });
29472 cx.assert_excerpts_with_selections(indoc! {"
29473 [EXCERPT]
29474 1
29475 2ˇ
29476 3
29477 [EXCERPT]
29478 1
29479 2
29480 3
29481 "});
29482
29483 cx.update_editor(|editor, window, cx| {
29484 editor
29485 .select_all_matches(&SelectAllMatches, window, cx)
29486 .unwrap();
29487 });
29488 cx.assert_excerpts_with_selections(indoc! {"
29489 [EXCERPT]
29490 1
29491 2ˇ
29492 3
29493 [EXCERPT]
29494 1
29495 2ˇ
29496 3
29497 "});
29498
29499 cx.update_editor(|editor, window, cx| {
29500 editor.handle_input("X", window, cx);
29501 });
29502 cx.assert_excerpts_with_selections(indoc! {"
29503 [EXCERPT]
29504 1
29505 Xˇ
29506 3
29507 [EXCERPT]
29508 1
29509 Xˇ
29510 3
29511 "});
29512
29513 // Scenario 2: Select "2", then fold second buffer before insertion
29514 cx.update_multibuffer(|mb, cx| {
29515 for buffer_id in buffer_ids.iter() {
29516 let buffer = mb.buffer(*buffer_id).unwrap();
29517 buffer.update(cx, |buffer, cx| {
29518 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
29519 });
29520 }
29521 });
29522
29523 // Select "2" and select all matches
29524 cx.update_editor(|editor, window, cx| {
29525 editor.change_selections(None.into(), window, cx, |s| {
29526 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29527 });
29528 editor
29529 .select_all_matches(&SelectAllMatches, window, cx)
29530 .unwrap();
29531 });
29532
29533 // Fold second buffer - should remove selections from folded buffer
29534 cx.update_editor(|editor, _, cx| {
29535 editor.fold_buffer(buffer_ids[1], cx);
29536 });
29537 cx.assert_excerpts_with_selections(indoc! {"
29538 [EXCERPT]
29539 1
29540 2ˇ
29541 3
29542 [EXCERPT]
29543 [FOLDED]
29544 "});
29545
29546 // Insert text - should only affect first buffer
29547 cx.update_editor(|editor, window, cx| {
29548 editor.handle_input("Y", window, cx);
29549 });
29550 cx.update_editor(|editor, _, cx| {
29551 editor.unfold_buffer(buffer_ids[1], cx);
29552 });
29553 cx.assert_excerpts_with_selections(indoc! {"
29554 [EXCERPT]
29555 1
29556 Yˇ
29557 3
29558 [EXCERPT]
29559 1
29560 2
29561 3
29562 "});
29563
29564 // Scenario 3: Select "2", then fold first buffer before insertion
29565 cx.update_multibuffer(|mb, cx| {
29566 for buffer_id in buffer_ids.iter() {
29567 let buffer = mb.buffer(*buffer_id).unwrap();
29568 buffer.update(cx, |buffer, cx| {
29569 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
29570 });
29571 }
29572 });
29573
29574 // Select "2" and select all matches
29575 cx.update_editor(|editor, window, cx| {
29576 editor.change_selections(None.into(), window, cx, |s| {
29577 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29578 });
29579 editor
29580 .select_all_matches(&SelectAllMatches, window, cx)
29581 .unwrap();
29582 });
29583
29584 // Fold first buffer - should remove selections from folded buffer
29585 cx.update_editor(|editor, _, cx| {
29586 editor.fold_buffer(buffer_ids[0], cx);
29587 });
29588 cx.assert_excerpts_with_selections(indoc! {"
29589 [EXCERPT]
29590 [FOLDED]
29591 [EXCERPT]
29592 1
29593 2ˇ
29594 3
29595 "});
29596
29597 // Insert text - should only affect second buffer
29598 cx.update_editor(|editor, window, cx| {
29599 editor.handle_input("Z", window, cx);
29600 });
29601 cx.update_editor(|editor, _, cx| {
29602 editor.unfold_buffer(buffer_ids[0], cx);
29603 });
29604 cx.assert_excerpts_with_selections(indoc! {"
29605 [EXCERPT]
29606 1
29607 2
29608 3
29609 [EXCERPT]
29610 1
29611 Zˇ
29612 3
29613 "});
29614
29615 // Test correct folded header is selected upon fold
29616 cx.update_editor(|editor, _, cx| {
29617 editor.fold_buffer(buffer_ids[0], cx);
29618 editor.fold_buffer(buffer_ids[1], cx);
29619 });
29620 cx.assert_excerpts_with_selections(indoc! {"
29621 [EXCERPT]
29622 [FOLDED]
29623 [EXCERPT]
29624 ˇ[FOLDED]
29625 "});
29626
29627 // Test selection inside folded buffer unfolds it on type
29628 cx.update_editor(|editor, window, cx| {
29629 editor.handle_input("W", window, cx);
29630 });
29631 cx.update_editor(|editor, _, cx| {
29632 editor.unfold_buffer(buffer_ids[0], cx);
29633 });
29634 cx.assert_excerpts_with_selections(indoc! {"
29635 [EXCERPT]
29636 1
29637 2
29638 3
29639 [EXCERPT]
29640 Wˇ1
29641 Z
29642 3
29643 "});
29644}
29645
29646#[gpui::test]
29647async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
29648 init_test(cx, |_| {});
29649
29650 let (editor, cx) = cx.add_window_view(|window, cx| {
29651 let multi_buffer = MultiBuffer::build_multi(
29652 [
29653 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29654 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
29655 ],
29656 cx,
29657 );
29658 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
29659 });
29660
29661 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29662
29663 cx.assert_excerpts_with_selections(indoc! {"
29664 [EXCERPT]
29665 ˇ1
29666 2
29667 3
29668 [EXCERPT]
29669 1
29670 2
29671 3
29672 4
29673 5
29674 6
29675 7
29676 8
29677 9
29678 "});
29679
29680 cx.update_editor(|editor, window, cx| {
29681 editor.change_selections(None.into(), window, cx, |s| {
29682 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
29683 });
29684 });
29685
29686 cx.assert_excerpts_with_selections(indoc! {"
29687 [EXCERPT]
29688 1
29689 2
29690 3
29691 [EXCERPT]
29692 1
29693 2
29694 3
29695 4
29696 5
29697 6
29698 ˇ7
29699 8
29700 9
29701 "});
29702
29703 cx.update_editor(|editor, _window, cx| {
29704 editor.set_vertical_scroll_margin(0, cx);
29705 });
29706
29707 cx.update_editor(|editor, window, cx| {
29708 assert_eq!(editor.vertical_scroll_margin(), 0);
29709 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29710 assert_eq!(
29711 editor.snapshot(window, cx).scroll_position(),
29712 gpui::Point::new(0., 12.0)
29713 );
29714 });
29715
29716 cx.update_editor(|editor, _window, cx| {
29717 editor.set_vertical_scroll_margin(3, cx);
29718 });
29719
29720 cx.update_editor(|editor, window, cx| {
29721 assert_eq!(editor.vertical_scroll_margin(), 3);
29722 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29723 assert_eq!(
29724 editor.snapshot(window, cx).scroll_position(),
29725 gpui::Point::new(0., 9.0)
29726 );
29727 });
29728}
29729
29730#[gpui::test]
29731async fn test_find_references_single_case(cx: &mut TestAppContext) {
29732 init_test(cx, |_| {});
29733 let mut cx = EditorLspTestContext::new_rust(
29734 lsp::ServerCapabilities {
29735 references_provider: Some(lsp::OneOf::Left(true)),
29736 ..lsp::ServerCapabilities::default()
29737 },
29738 cx,
29739 )
29740 .await;
29741
29742 let before = indoc!(
29743 r#"
29744 fn main() {
29745 let aˇbc = 123;
29746 let xyz = abc;
29747 }
29748 "#
29749 );
29750 let after = indoc!(
29751 r#"
29752 fn main() {
29753 let abc = 123;
29754 let xyz = ˇabc;
29755 }
29756 "#
29757 );
29758
29759 cx.lsp
29760 .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
29761 Ok(Some(vec![
29762 lsp::Location {
29763 uri: params.text_document_position.text_document.uri.clone(),
29764 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 11)),
29765 },
29766 lsp::Location {
29767 uri: params.text_document_position.text_document.uri,
29768 range: lsp::Range::new(lsp::Position::new(2, 14), lsp::Position::new(2, 17)),
29769 },
29770 ]))
29771 });
29772
29773 cx.set_state(before);
29774
29775 let action = FindAllReferences {
29776 always_open_multibuffer: false,
29777 };
29778
29779 let navigated = cx
29780 .update_editor(|editor, window, cx| editor.find_all_references(&action, window, cx))
29781 .expect("should have spawned a task")
29782 .await
29783 .unwrap();
29784
29785 assert_eq!(navigated, Navigated::No);
29786
29787 cx.run_until_parked();
29788
29789 cx.assert_editor_state(after);
29790}
29791
29792#[gpui::test]
29793async fn test_newline_task_list_continuation(cx: &mut TestAppContext) {
29794 init_test(cx, |settings| {
29795 settings.defaults.tab_size = Some(2.try_into().unwrap());
29796 });
29797
29798 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29799 let mut cx = EditorTestContext::new(cx).await;
29800 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29801
29802 // Case 1: Adding newline after (whitespace + prefix + any non-whitespace) adds marker
29803 cx.set_state(indoc! {"
29804 - [ ] taskˇ
29805 "});
29806 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29807 cx.wait_for_autoindent_applied().await;
29808 cx.assert_editor_state(indoc! {"
29809 - [ ] task
29810 - [ ] ˇ
29811 "});
29812
29813 // Case 2: Works with checked task items too
29814 cx.set_state(indoc! {"
29815 - [x] completed taskˇ
29816 "});
29817 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29818 cx.wait_for_autoindent_applied().await;
29819 cx.assert_editor_state(indoc! {"
29820 - [x] completed task
29821 - [ ] ˇ
29822 "});
29823
29824 // Case 2.1: Works with uppercase checked marker too
29825 cx.set_state(indoc! {"
29826 - [X] completed taskˇ
29827 "});
29828 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29829 cx.wait_for_autoindent_applied().await;
29830 cx.assert_editor_state(indoc! {"
29831 - [X] completed task
29832 - [ ] ˇ
29833 "});
29834
29835 // Case 3: Cursor position doesn't matter - content after marker is what counts
29836 cx.set_state(indoc! {"
29837 - [ ] taˇsk
29838 "});
29839 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29840 cx.wait_for_autoindent_applied().await;
29841 cx.assert_editor_state(indoc! {"
29842 - [ ] ta
29843 - [ ] ˇsk
29844 "});
29845
29846 // Case 4: Adding newline after (whitespace + prefix + some whitespace) does NOT add marker
29847 cx.set_state(indoc! {"
29848 - [ ] ˇ
29849 "});
29850 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29851 cx.wait_for_autoindent_applied().await;
29852 cx.assert_editor_state(
29853 indoc! {"
29854 - [ ]$$
29855 ˇ
29856 "}
29857 .replace("$", " ")
29858 .as_str(),
29859 );
29860
29861 // Case 5: Adding newline with content adds marker preserving indentation
29862 cx.set_state(indoc! {"
29863 - [ ] task
29864 - [ ] indentedˇ
29865 "});
29866 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29867 cx.wait_for_autoindent_applied().await;
29868 cx.assert_editor_state(indoc! {"
29869 - [ ] task
29870 - [ ] indented
29871 - [ ] ˇ
29872 "});
29873
29874 // Case 6: Adding newline with cursor right after prefix, unindents
29875 cx.set_state(indoc! {"
29876 - [ ] task
29877 - [ ] sub task
29878 - [ ] ˇ
29879 "});
29880 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29881 cx.wait_for_autoindent_applied().await;
29882 cx.assert_editor_state(indoc! {"
29883 - [ ] task
29884 - [ ] sub task
29885 - [ ] ˇ
29886 "});
29887 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29888 cx.wait_for_autoindent_applied().await;
29889
29890 // Case 7: Adding newline with cursor right after prefix, removes marker
29891 cx.assert_editor_state(indoc! {"
29892 - [ ] task
29893 - [ ] sub task
29894 - [ ] ˇ
29895 "});
29896 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29897 cx.wait_for_autoindent_applied().await;
29898 cx.assert_editor_state(indoc! {"
29899 - [ ] task
29900 - [ ] sub task
29901 ˇ
29902 "});
29903
29904 // Case 8: Cursor before or inside prefix does not add marker
29905 cx.set_state(indoc! {"
29906 ˇ- [ ] task
29907 "});
29908 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29909 cx.wait_for_autoindent_applied().await;
29910 cx.assert_editor_state(indoc! {"
29911
29912 ˇ- [ ] task
29913 "});
29914
29915 cx.set_state(indoc! {"
29916 - [ˇ ] task
29917 "});
29918 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29919 cx.wait_for_autoindent_applied().await;
29920 cx.assert_editor_state(indoc! {"
29921 - [
29922 ˇ
29923 ] task
29924 "});
29925}
29926
29927#[gpui::test]
29928async fn test_newline_unordered_list_continuation(cx: &mut TestAppContext) {
29929 init_test(cx, |settings| {
29930 settings.defaults.tab_size = Some(2.try_into().unwrap());
29931 });
29932
29933 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29934 let mut cx = EditorTestContext::new(cx).await;
29935 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29936
29937 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) adds marker
29938 cx.set_state(indoc! {"
29939 - itemˇ
29940 "});
29941 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29942 cx.wait_for_autoindent_applied().await;
29943 cx.assert_editor_state(indoc! {"
29944 - item
29945 - ˇ
29946 "});
29947
29948 // Case 2: Works with different markers
29949 cx.set_state(indoc! {"
29950 * starred itemˇ
29951 "});
29952 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29953 cx.wait_for_autoindent_applied().await;
29954 cx.assert_editor_state(indoc! {"
29955 * starred item
29956 * ˇ
29957 "});
29958
29959 cx.set_state(indoc! {"
29960 + plus itemˇ
29961 "});
29962 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29963 cx.wait_for_autoindent_applied().await;
29964 cx.assert_editor_state(indoc! {"
29965 + plus item
29966 + ˇ
29967 "});
29968
29969 // Case 3: Cursor position doesn't matter - content after marker is what counts
29970 cx.set_state(indoc! {"
29971 - itˇem
29972 "});
29973 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29974 cx.wait_for_autoindent_applied().await;
29975 cx.assert_editor_state(indoc! {"
29976 - it
29977 - ˇem
29978 "});
29979
29980 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
29981 cx.set_state(indoc! {"
29982 - ˇ
29983 "});
29984 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29985 cx.wait_for_autoindent_applied().await;
29986 cx.assert_editor_state(
29987 indoc! {"
29988 - $
29989 ˇ
29990 "}
29991 .replace("$", " ")
29992 .as_str(),
29993 );
29994
29995 // Case 5: Adding newline with content adds marker preserving indentation
29996 cx.set_state(indoc! {"
29997 - item
29998 - indentedˇ
29999 "});
30000 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30001 cx.wait_for_autoindent_applied().await;
30002 cx.assert_editor_state(indoc! {"
30003 - item
30004 - indented
30005 - ˇ
30006 "});
30007
30008 // Case 6: Adding newline with cursor right after marker, unindents
30009 cx.set_state(indoc! {"
30010 - item
30011 - sub item
30012 - ˇ
30013 "});
30014 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30015 cx.wait_for_autoindent_applied().await;
30016 cx.assert_editor_state(indoc! {"
30017 - item
30018 - sub item
30019 - ˇ
30020 "});
30021 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30022 cx.wait_for_autoindent_applied().await;
30023
30024 // Case 7: Adding newline with cursor right after marker, removes marker
30025 cx.assert_editor_state(indoc! {"
30026 - item
30027 - sub item
30028 - ˇ
30029 "});
30030 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30031 cx.wait_for_autoindent_applied().await;
30032 cx.assert_editor_state(indoc! {"
30033 - item
30034 - sub item
30035 ˇ
30036 "});
30037
30038 // Case 8: Cursor before or inside prefix does not add marker
30039 cx.set_state(indoc! {"
30040 ˇ- item
30041 "});
30042 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30043 cx.wait_for_autoindent_applied().await;
30044 cx.assert_editor_state(indoc! {"
30045
30046 ˇ- item
30047 "});
30048
30049 cx.set_state(indoc! {"
30050 -ˇ item
30051 "});
30052 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30053 cx.wait_for_autoindent_applied().await;
30054 cx.assert_editor_state(indoc! {"
30055 -
30056 ˇitem
30057 "});
30058}
30059
30060#[gpui::test]
30061async fn test_newline_ordered_list_continuation(cx: &mut TestAppContext) {
30062 init_test(cx, |settings| {
30063 settings.defaults.tab_size = Some(2.try_into().unwrap());
30064 });
30065
30066 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
30067 let mut cx = EditorTestContext::new(cx).await;
30068 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
30069
30070 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
30071 cx.set_state(indoc! {"
30072 1. first itemˇ
30073 "});
30074 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30075 cx.wait_for_autoindent_applied().await;
30076 cx.assert_editor_state(indoc! {"
30077 1. first item
30078 2. ˇ
30079 "});
30080
30081 // Case 2: Works with larger numbers
30082 cx.set_state(indoc! {"
30083 10. tenth itemˇ
30084 "});
30085 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30086 cx.wait_for_autoindent_applied().await;
30087 cx.assert_editor_state(indoc! {"
30088 10. tenth item
30089 11. ˇ
30090 "});
30091
30092 // Case 3: Cursor position doesn't matter - content after marker is what counts
30093 cx.set_state(indoc! {"
30094 1. itˇem
30095 "});
30096 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30097 cx.wait_for_autoindent_applied().await;
30098 cx.assert_editor_state(indoc! {"
30099 1. it
30100 2. ˇem
30101 "});
30102
30103 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
30104 cx.set_state(indoc! {"
30105 1. ˇ
30106 "});
30107 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30108 cx.wait_for_autoindent_applied().await;
30109 cx.assert_editor_state(
30110 indoc! {"
30111 1. $
30112 ˇ
30113 "}
30114 .replace("$", " ")
30115 .as_str(),
30116 );
30117
30118 // Case 5: Adding newline with content adds marker preserving indentation
30119 cx.set_state(indoc! {"
30120 1. item
30121 2. indentedˇ
30122 "});
30123 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30124 cx.wait_for_autoindent_applied().await;
30125 cx.assert_editor_state(indoc! {"
30126 1. item
30127 2. indented
30128 3. ˇ
30129 "});
30130
30131 // Case 6: Adding newline with cursor right after marker, unindents
30132 cx.set_state(indoc! {"
30133 1. item
30134 2. sub item
30135 3. ˇ
30136 "});
30137 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30138 cx.wait_for_autoindent_applied().await;
30139 cx.assert_editor_state(indoc! {"
30140 1. item
30141 2. sub item
30142 1. ˇ
30143 "});
30144 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30145 cx.wait_for_autoindent_applied().await;
30146
30147 // Case 7: Adding newline with cursor right after marker, removes marker
30148 cx.assert_editor_state(indoc! {"
30149 1. item
30150 2. sub item
30151 1. ˇ
30152 "});
30153 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30154 cx.wait_for_autoindent_applied().await;
30155 cx.assert_editor_state(indoc! {"
30156 1. item
30157 2. sub item
30158 ˇ
30159 "});
30160
30161 // Case 8: Cursor before or inside prefix does not add marker
30162 cx.set_state(indoc! {"
30163 ˇ1. item
30164 "});
30165 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30166 cx.wait_for_autoindent_applied().await;
30167 cx.assert_editor_state(indoc! {"
30168
30169 ˇ1. item
30170 "});
30171
30172 cx.set_state(indoc! {"
30173 1ˇ. item
30174 "});
30175 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30176 cx.wait_for_autoindent_applied().await;
30177 cx.assert_editor_state(indoc! {"
30178 1
30179 ˇ. item
30180 "});
30181}
30182
30183#[gpui::test]
30184async fn test_newline_should_not_autoindent_ordered_list(cx: &mut TestAppContext) {
30185 init_test(cx, |settings| {
30186 settings.defaults.tab_size = Some(2.try_into().unwrap());
30187 });
30188
30189 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
30190 let mut cx = EditorTestContext::new(cx).await;
30191 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
30192
30193 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
30194 cx.set_state(indoc! {"
30195 1. first item
30196 1. sub first item
30197 2. sub second item
30198 3. ˇ
30199 "});
30200 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30201 cx.wait_for_autoindent_applied().await;
30202 cx.assert_editor_state(indoc! {"
30203 1. first item
30204 1. sub first item
30205 2. sub second item
30206 1. ˇ
30207 "});
30208}
30209
30210#[gpui::test]
30211async fn test_tab_list_indent(cx: &mut TestAppContext) {
30212 init_test(cx, |settings| {
30213 settings.defaults.tab_size = Some(2.try_into().unwrap());
30214 });
30215
30216 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
30217 let mut cx = EditorTestContext::new(cx).await;
30218 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
30219
30220 // Case 1: Unordered list - cursor after prefix, adds indent before prefix
30221 cx.set_state(indoc! {"
30222 - ˇitem
30223 "});
30224 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30225 cx.wait_for_autoindent_applied().await;
30226 let expected = indoc! {"
30227 $$- ˇitem
30228 "};
30229 cx.assert_editor_state(expected.replace("$", " ").as_str());
30230
30231 // Case 2: Task list - cursor after prefix
30232 cx.set_state(indoc! {"
30233 - [ ] ˇtask
30234 "});
30235 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30236 cx.wait_for_autoindent_applied().await;
30237 let expected = indoc! {"
30238 $$- [ ] ˇtask
30239 "};
30240 cx.assert_editor_state(expected.replace("$", " ").as_str());
30241
30242 // Case 3: Ordered list - cursor after prefix
30243 cx.set_state(indoc! {"
30244 1. ˇfirst
30245 "});
30246 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30247 cx.wait_for_autoindent_applied().await;
30248 let expected = indoc! {"
30249 $$1. ˇfirst
30250 "};
30251 cx.assert_editor_state(expected.replace("$", " ").as_str());
30252
30253 // Case 4: With existing indentation - adds more indent
30254 let initial = indoc! {"
30255 $$- ˇitem
30256 "};
30257 cx.set_state(initial.replace("$", " ").as_str());
30258 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30259 cx.wait_for_autoindent_applied().await;
30260 let expected = indoc! {"
30261 $$$$- ˇitem
30262 "};
30263 cx.assert_editor_state(expected.replace("$", " ").as_str());
30264
30265 // Case 5: Empty list item
30266 cx.set_state(indoc! {"
30267 - ˇ
30268 "});
30269 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30270 cx.wait_for_autoindent_applied().await;
30271 let expected = indoc! {"
30272 $$- ˇ
30273 "};
30274 cx.assert_editor_state(expected.replace("$", " ").as_str());
30275
30276 // Case 6: Cursor at end of line with content
30277 cx.set_state(indoc! {"
30278 - itemˇ
30279 "});
30280 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30281 cx.wait_for_autoindent_applied().await;
30282 let expected = indoc! {"
30283 $$- itemˇ
30284 "};
30285 cx.assert_editor_state(expected.replace("$", " ").as_str());
30286
30287 // Case 7: Cursor at start of list item, indents it
30288 cx.set_state(indoc! {"
30289 - item
30290 ˇ - sub item
30291 "});
30292 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30293 cx.wait_for_autoindent_applied().await;
30294 let expected = indoc! {"
30295 - item
30296 ˇ - sub item
30297 "};
30298 cx.assert_editor_state(expected);
30299
30300 // Case 8: Cursor at start of list item, moves the cursor when "indent_list_on_tab" is false
30301 cx.update_editor(|_, _, cx| {
30302 SettingsStore::update_global(cx, |store, cx| {
30303 store.update_user_settings(cx, |settings| {
30304 settings.project.all_languages.defaults.indent_list_on_tab = Some(false);
30305 });
30306 });
30307 });
30308 cx.set_state(indoc! {"
30309 - item
30310 ˇ - sub item
30311 "});
30312 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30313 cx.wait_for_autoindent_applied().await;
30314 let expected = indoc! {"
30315 - item
30316 ˇ- sub item
30317 "};
30318 cx.assert_editor_state(expected);
30319}
30320
30321#[gpui::test]
30322async fn test_local_worktree_trust(cx: &mut TestAppContext) {
30323 init_test(cx, |_| {});
30324 cx.update(|cx| project::trusted_worktrees::init(HashMap::default(), cx));
30325
30326 cx.update(|cx| {
30327 SettingsStore::update_global(cx, |store, cx| {
30328 store.update_user_settings(cx, |settings| {
30329 settings.project.all_languages.defaults.inlay_hints =
30330 Some(InlayHintSettingsContent {
30331 enabled: Some(true),
30332 ..InlayHintSettingsContent::default()
30333 });
30334 });
30335 });
30336 });
30337
30338 let fs = FakeFs::new(cx.executor());
30339 fs.insert_tree(
30340 path!("/project"),
30341 json!({
30342 ".zed": {
30343 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
30344 },
30345 "main.rs": "fn main() {}"
30346 }),
30347 )
30348 .await;
30349
30350 let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
30351 let server_name = "override-rust-analyzer";
30352 let project = Project::test_with_worktree_trust(fs, [path!("/project").as_ref()], cx).await;
30353
30354 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
30355 language_registry.add(rust_lang());
30356
30357 let capabilities = lsp::ServerCapabilities {
30358 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
30359 ..lsp::ServerCapabilities::default()
30360 };
30361 let mut fake_language_servers = language_registry.register_fake_lsp(
30362 "Rust",
30363 FakeLspAdapter {
30364 name: server_name,
30365 capabilities,
30366 initializer: Some(Box::new({
30367 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
30368 move |fake_server| {
30369 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
30370 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
30371 move |_params, _| {
30372 lsp_inlay_hint_request_count.fetch_add(1, atomic::Ordering::Release);
30373 async move {
30374 Ok(Some(vec![lsp::InlayHint {
30375 position: lsp::Position::new(0, 0),
30376 label: lsp::InlayHintLabel::String("hint".to_string()),
30377 kind: None,
30378 text_edits: None,
30379 tooltip: None,
30380 padding_left: None,
30381 padding_right: None,
30382 data: None,
30383 }]))
30384 }
30385 },
30386 );
30387 }
30388 })),
30389 ..FakeLspAdapter::default()
30390 },
30391 );
30392
30393 cx.run_until_parked();
30394
30395 let worktree_id = project.read_with(cx, |project, cx| {
30396 project
30397 .worktrees(cx)
30398 .next()
30399 .map(|wt| wt.read(cx).id())
30400 .expect("should have a worktree")
30401 });
30402 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
30403
30404 let trusted_worktrees =
30405 cx.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
30406
30407 let can_trust = trusted_worktrees.update(cx, |store, cx| {
30408 store.can_trust(&worktree_store, worktree_id, cx)
30409 });
30410 assert!(!can_trust, "worktree should be restricted initially");
30411
30412 let buffer_before_approval = project
30413 .update(cx, |project, cx| {
30414 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
30415 })
30416 .await
30417 .unwrap();
30418
30419 let (editor, cx) = cx.add_window_view(|window, cx| {
30420 Editor::new(
30421 EditorMode::full(),
30422 cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
30423 Some(project.clone()),
30424 window,
30425 cx,
30426 )
30427 });
30428 cx.run_until_parked();
30429 let fake_language_server = fake_language_servers.next();
30430
30431 cx.read(|cx| {
30432 let file = buffer_before_approval.read(cx).file();
30433 assert_eq!(
30434 language::language_settings::language_settings(Some("Rust".into()), file, cx)
30435 .language_servers,
30436 ["...".to_string()],
30437 "local .zed/settings.json must not apply before trust approval"
30438 )
30439 });
30440
30441 editor.update_in(cx, |editor, window, cx| {
30442 editor.handle_input("1", window, cx);
30443 });
30444 cx.run_until_parked();
30445 cx.executor()
30446 .advance_clock(std::time::Duration::from_secs(1));
30447 assert_eq!(
30448 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire),
30449 0,
30450 "inlay hints must not be queried before trust approval"
30451 );
30452
30453 trusted_worktrees.update(cx, |store, cx| {
30454 store.trust(
30455 &worktree_store,
30456 std::collections::HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
30457 cx,
30458 );
30459 });
30460 cx.run_until_parked();
30461
30462 cx.read(|cx| {
30463 let file = buffer_before_approval.read(cx).file();
30464 assert_eq!(
30465 language::language_settings::language_settings(Some("Rust".into()), file, cx)
30466 .language_servers,
30467 ["override-rust-analyzer".to_string()],
30468 "local .zed/settings.json should apply after trust approval"
30469 )
30470 });
30471 let _fake_language_server = fake_language_server.await.unwrap();
30472 editor.update_in(cx, |editor, window, cx| {
30473 editor.handle_input("1", window, cx);
30474 });
30475 cx.run_until_parked();
30476 cx.executor()
30477 .advance_clock(std::time::Duration::from_secs(1));
30478 assert!(
30479 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire) > 0,
30480 "inlay hints should be queried after trust approval"
30481 );
30482
30483 let can_trust_after = trusted_worktrees.update(cx, |store, cx| {
30484 store.can_trust(&worktree_store, worktree_id, cx)
30485 });
30486 assert!(can_trust_after, "worktree should be trusted after trust()");
30487}
30488
30489#[gpui::test]
30490fn test_editor_rendering_when_positioned_above_viewport(cx: &mut TestAppContext) {
30491 // This test reproduces a bug where drawing an editor at a position above the viewport
30492 // (simulating what happens when an AutoHeight editor inside a List is scrolled past)
30493 // causes an infinite loop in blocks_in_range.
30494 //
30495 // The issue: when the editor's bounds.origin.y is very negative (above the viewport),
30496 // the content mask intersection produces visible_bounds with origin at the viewport top.
30497 // This makes clipped_top_in_lines very large, causing start_row to exceed max_row.
30498 // When blocks_in_range is called with start_row > max_row, the cursor seeks to the end
30499 // but the while loop after seek never terminates because cursor.next() is a no-op at end.
30500 init_test(cx, |_| {});
30501
30502 let window = cx.add_window(|_, _| gpui::Empty);
30503 let mut cx = VisualTestContext::from_window(*window, cx);
30504
30505 let buffer = cx.update(|_, cx| MultiBuffer::build_simple("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n", cx));
30506 let editor = cx.new_window_entity(|window, cx| build_editor(buffer, window, cx));
30507
30508 // Simulate a small viewport (500x500 pixels at origin 0,0)
30509 cx.simulate_resize(gpui::size(px(500.), px(500.)));
30510
30511 // Draw the editor at a very negative Y position, simulating an editor that's been
30512 // scrolled way above the visible viewport (like in a List that has scrolled past it).
30513 // The editor is 3000px tall but positioned at y=-10000, so it's entirely above the viewport.
30514 // This should NOT hang - it should just render nothing.
30515 cx.draw(
30516 gpui::point(px(0.), px(-10000.)),
30517 gpui::size(px(500.), px(3000.)),
30518 |_, _| editor.clone().into_any_element(),
30519 );
30520
30521 // If we get here without hanging, the test passes
30522}
30523
30524#[gpui::test]
30525async fn test_diff_review_indicator_created_on_gutter_hover(cx: &mut TestAppContext) {
30526 init_test(cx, |_| {});
30527
30528 let fs = FakeFs::new(cx.executor());
30529 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
30530 .await;
30531
30532 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
30533 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
30534 let cx = &mut VisualTestContext::from_window(*workspace, cx);
30535
30536 let editor = workspace
30537 .update(cx, |workspace, window, cx| {
30538 workspace.open_abs_path(
30539 PathBuf::from(path!("/root/file.txt")),
30540 OpenOptions::default(),
30541 window,
30542 cx,
30543 )
30544 })
30545 .unwrap()
30546 .await
30547 .unwrap()
30548 .downcast::<Editor>()
30549 .unwrap();
30550
30551 // Enable diff review button mode
30552 editor.update(cx, |editor, cx| {
30553 editor.set_show_diff_review_button(true, cx);
30554 });
30555
30556 // Initially, no indicator should be present
30557 editor.update(cx, |editor, _cx| {
30558 assert!(
30559 editor.gutter_diff_review_indicator.0.is_none(),
30560 "Indicator should be None initially"
30561 );
30562 });
30563}
30564
30565#[gpui::test]
30566async fn test_diff_review_button_hidden_when_ai_disabled(cx: &mut TestAppContext) {
30567 init_test(cx, |_| {});
30568
30569 // Register DisableAiSettings and set disable_ai to true
30570 cx.update(|cx| {
30571 project::DisableAiSettings::register(cx);
30572 project::DisableAiSettings::override_global(
30573 project::DisableAiSettings { disable_ai: true },
30574 cx,
30575 );
30576 });
30577
30578 let fs = FakeFs::new(cx.executor());
30579 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
30580 .await;
30581
30582 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
30583 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
30584 let cx = &mut VisualTestContext::from_window(*workspace, cx);
30585
30586 let editor = workspace
30587 .update(cx, |workspace, window, cx| {
30588 workspace.open_abs_path(
30589 PathBuf::from(path!("/root/file.txt")),
30590 OpenOptions::default(),
30591 window,
30592 cx,
30593 )
30594 })
30595 .unwrap()
30596 .await
30597 .unwrap()
30598 .downcast::<Editor>()
30599 .unwrap();
30600
30601 // Enable diff review button mode
30602 editor.update(cx, |editor, cx| {
30603 editor.set_show_diff_review_button(true, cx);
30604 });
30605
30606 // Verify AI is disabled
30607 cx.read(|cx| {
30608 assert!(
30609 project::DisableAiSettings::get_global(cx).disable_ai,
30610 "AI should be disabled"
30611 );
30612 });
30613
30614 // The indicator should not be created when AI is disabled
30615 // (The mouse_moved handler checks DisableAiSettings before creating the indicator)
30616 editor.update(cx, |editor, _cx| {
30617 assert!(
30618 editor.gutter_diff_review_indicator.0.is_none(),
30619 "Indicator should be None when AI is disabled"
30620 );
30621 });
30622}
30623
30624#[gpui::test]
30625async fn test_diff_review_button_shown_when_ai_enabled(cx: &mut TestAppContext) {
30626 init_test(cx, |_| {});
30627
30628 // Register DisableAiSettings and set disable_ai to false
30629 cx.update(|cx| {
30630 project::DisableAiSettings::register(cx);
30631 project::DisableAiSettings::override_global(
30632 project::DisableAiSettings { disable_ai: false },
30633 cx,
30634 );
30635 });
30636
30637 let fs = FakeFs::new(cx.executor());
30638 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
30639 .await;
30640
30641 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
30642 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
30643 let cx = &mut VisualTestContext::from_window(*workspace, cx);
30644
30645 let editor = workspace
30646 .update(cx, |workspace, window, cx| {
30647 workspace.open_abs_path(
30648 PathBuf::from(path!("/root/file.txt")),
30649 OpenOptions::default(),
30650 window,
30651 cx,
30652 )
30653 })
30654 .unwrap()
30655 .await
30656 .unwrap()
30657 .downcast::<Editor>()
30658 .unwrap();
30659
30660 // Enable diff review button mode
30661 editor.update(cx, |editor, cx| {
30662 editor.set_show_diff_review_button(true, cx);
30663 });
30664
30665 // Verify AI is enabled
30666 cx.read(|cx| {
30667 assert!(
30668 !project::DisableAiSettings::get_global(cx).disable_ai,
30669 "AI should be enabled"
30670 );
30671 });
30672
30673 // The show_diff_review_button flag should be true
30674 editor.update(cx, |editor, _cx| {
30675 assert!(
30676 editor.show_diff_review_button(),
30677 "show_diff_review_button should be true"
30678 );
30679 });
30680}
30681
30682/// Helper function to create a DiffHunkKey for testing.
30683/// Uses Anchor::min() as a placeholder anchor since these tests don't need
30684/// real buffer positioning.
30685fn test_hunk_key(file_path: &str) -> DiffHunkKey {
30686 DiffHunkKey {
30687 file_path: if file_path.is_empty() {
30688 Arc::from(util::rel_path::RelPath::empty())
30689 } else {
30690 Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
30691 },
30692 hunk_start_anchor: Anchor::min(),
30693 }
30694}
30695
30696/// Helper function to create a DiffHunkKey with a specific anchor for testing.
30697fn test_hunk_key_with_anchor(file_path: &str, anchor: Anchor) -> DiffHunkKey {
30698 DiffHunkKey {
30699 file_path: if file_path.is_empty() {
30700 Arc::from(util::rel_path::RelPath::empty())
30701 } else {
30702 Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
30703 },
30704 hunk_start_anchor: anchor,
30705 }
30706}
30707
30708/// Helper function to add a review comment with default anchors for testing.
30709fn add_test_comment(
30710 editor: &mut Editor,
30711 key: DiffHunkKey,
30712 comment: &str,
30713 cx: &mut Context<Editor>,
30714) -> usize {
30715 editor.add_review_comment(key, comment.to_string(), Anchor::min()..Anchor::max(), cx)
30716}
30717
30718#[gpui::test]
30719fn test_review_comment_add_to_hunk(cx: &mut TestAppContext) {
30720 init_test(cx, |_| {});
30721
30722 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30723
30724 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30725 let key = test_hunk_key("");
30726
30727 let id = add_test_comment(editor, key.clone(), "Test comment", cx);
30728
30729 let snapshot = editor.buffer().read(cx).snapshot(cx);
30730 assert_eq!(editor.total_review_comment_count(), 1);
30731 assert_eq!(editor.hunk_comment_count(&key, &snapshot), 1);
30732
30733 let comments = editor.comments_for_hunk(&key, &snapshot);
30734 assert_eq!(comments.len(), 1);
30735 assert_eq!(comments[0].comment, "Test comment");
30736 assert_eq!(comments[0].id, id);
30737 });
30738}
30739
30740#[gpui::test]
30741fn test_review_comments_are_per_hunk(cx: &mut TestAppContext) {
30742 init_test(cx, |_| {});
30743
30744 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30745
30746 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30747 let snapshot = editor.buffer().read(cx).snapshot(cx);
30748 let anchor1 = snapshot.anchor_before(Point::new(0, 0));
30749 let anchor2 = snapshot.anchor_before(Point::new(0, 0));
30750 let key1 = test_hunk_key_with_anchor("file1.rs", anchor1);
30751 let key2 = test_hunk_key_with_anchor("file2.rs", anchor2);
30752
30753 add_test_comment(editor, key1.clone(), "Comment for file1", cx);
30754 add_test_comment(editor, key2.clone(), "Comment for file2", cx);
30755
30756 let snapshot = editor.buffer().read(cx).snapshot(cx);
30757 assert_eq!(editor.total_review_comment_count(), 2);
30758 assert_eq!(editor.hunk_comment_count(&key1, &snapshot), 1);
30759 assert_eq!(editor.hunk_comment_count(&key2, &snapshot), 1);
30760
30761 assert_eq!(
30762 editor.comments_for_hunk(&key1, &snapshot)[0].comment,
30763 "Comment for file1"
30764 );
30765 assert_eq!(
30766 editor.comments_for_hunk(&key2, &snapshot)[0].comment,
30767 "Comment for file2"
30768 );
30769 });
30770}
30771
30772#[gpui::test]
30773fn test_review_comment_remove(cx: &mut TestAppContext) {
30774 init_test(cx, |_| {});
30775
30776 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30777
30778 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30779 let key = test_hunk_key("");
30780
30781 let id = add_test_comment(editor, key, "To be removed", cx);
30782
30783 assert_eq!(editor.total_review_comment_count(), 1);
30784
30785 let removed = editor.remove_review_comment(id, cx);
30786 assert!(removed);
30787 assert_eq!(editor.total_review_comment_count(), 0);
30788
30789 // Try to remove again
30790 let removed_again = editor.remove_review_comment(id, cx);
30791 assert!(!removed_again);
30792 });
30793}
30794
30795#[gpui::test]
30796fn test_review_comment_update(cx: &mut TestAppContext) {
30797 init_test(cx, |_| {});
30798
30799 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30800
30801 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30802 let key = test_hunk_key("");
30803
30804 let id = add_test_comment(editor, key.clone(), "Original text", cx);
30805
30806 let updated = editor.update_review_comment(id, "Updated text".to_string(), cx);
30807 assert!(updated);
30808
30809 let snapshot = editor.buffer().read(cx).snapshot(cx);
30810 let comments = editor.comments_for_hunk(&key, &snapshot);
30811 assert_eq!(comments[0].comment, "Updated text");
30812 assert!(!comments[0].is_editing); // Should clear editing flag
30813 });
30814}
30815
30816#[gpui::test]
30817fn test_review_comment_take_all(cx: &mut TestAppContext) {
30818 init_test(cx, |_| {});
30819
30820 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30821
30822 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30823 let snapshot = editor.buffer().read(cx).snapshot(cx);
30824 let anchor1 = snapshot.anchor_before(Point::new(0, 0));
30825 let anchor2 = snapshot.anchor_before(Point::new(0, 0));
30826 let key1 = test_hunk_key_with_anchor("file1.rs", anchor1);
30827 let key2 = test_hunk_key_with_anchor("file2.rs", anchor2);
30828
30829 let id1 = add_test_comment(editor, key1.clone(), "Comment 1", cx);
30830 let id2 = add_test_comment(editor, key1.clone(), "Comment 2", cx);
30831 let id3 = add_test_comment(editor, key2.clone(), "Comment 3", cx);
30832
30833 // IDs should be sequential starting from 0
30834 assert_eq!(id1, 0);
30835 assert_eq!(id2, 1);
30836 assert_eq!(id3, 2);
30837
30838 assert_eq!(editor.total_review_comment_count(), 3);
30839
30840 let taken = editor.take_all_review_comments(cx);
30841
30842 // Should have 2 entries (one per hunk)
30843 assert_eq!(taken.len(), 2);
30844
30845 // Total comments should be 3
30846 let total: usize = taken
30847 .iter()
30848 .map(|(_, comments): &(DiffHunkKey, Vec<StoredReviewComment>)| comments.len())
30849 .sum();
30850 assert_eq!(total, 3);
30851
30852 // Storage should be empty
30853 assert_eq!(editor.total_review_comment_count(), 0);
30854
30855 // After taking all comments, ID counter should reset
30856 // New comments should get IDs starting from 0 again
30857 let new_id1 = add_test_comment(editor, key1, "New Comment 1", cx);
30858 let new_id2 = add_test_comment(editor, key2, "New Comment 2", cx);
30859
30860 assert_eq!(new_id1, 0, "ID counter should reset after take_all");
30861 assert_eq!(new_id2, 1, "IDs should be sequential after reset");
30862 });
30863}
30864
30865#[gpui::test]
30866fn test_diff_review_overlay_show_and_dismiss(cx: &mut TestAppContext) {
30867 init_test(cx, |_| {});
30868
30869 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30870
30871 // Show overlay
30872 editor
30873 .update(cx, |editor, window, cx| {
30874 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
30875 })
30876 .unwrap();
30877
30878 // Verify overlay is shown
30879 editor
30880 .update(cx, |editor, _window, cx| {
30881 assert!(!editor.diff_review_overlays.is_empty());
30882 assert_eq!(editor.diff_review_line_range(cx), Some((0, 0)));
30883 assert!(editor.diff_review_prompt_editor().is_some());
30884 })
30885 .unwrap();
30886
30887 // Dismiss overlay
30888 editor
30889 .update(cx, |editor, _window, cx| {
30890 editor.dismiss_all_diff_review_overlays(cx);
30891 })
30892 .unwrap();
30893
30894 // Verify overlay is dismissed
30895 editor
30896 .update(cx, |editor, _window, cx| {
30897 assert!(editor.diff_review_overlays.is_empty());
30898 assert_eq!(editor.diff_review_line_range(cx), None);
30899 assert!(editor.diff_review_prompt_editor().is_none());
30900 })
30901 .unwrap();
30902}
30903
30904#[gpui::test]
30905fn test_diff_review_overlay_dismiss_via_cancel(cx: &mut TestAppContext) {
30906 init_test(cx, |_| {});
30907
30908 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30909
30910 // Show overlay
30911 editor
30912 .update(cx, |editor, window, cx| {
30913 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
30914 })
30915 .unwrap();
30916
30917 // Verify overlay is shown
30918 editor
30919 .update(cx, |editor, _window, _cx| {
30920 assert!(!editor.diff_review_overlays.is_empty());
30921 })
30922 .unwrap();
30923
30924 // Dismiss via dismiss_menus_and_popups (which is called by cancel action)
30925 editor
30926 .update(cx, |editor, window, cx| {
30927 editor.dismiss_menus_and_popups(true, window, cx);
30928 })
30929 .unwrap();
30930
30931 // Verify overlay is dismissed
30932 editor
30933 .update(cx, |editor, _window, _cx| {
30934 assert!(editor.diff_review_overlays.is_empty());
30935 })
30936 .unwrap();
30937}
30938
30939#[gpui::test]
30940fn test_diff_review_empty_comment_not_submitted(cx: &mut TestAppContext) {
30941 init_test(cx, |_| {});
30942
30943 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30944
30945 // Show overlay
30946 editor
30947 .update(cx, |editor, window, cx| {
30948 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
30949 })
30950 .unwrap();
30951
30952 // Try to submit without typing anything (empty comment)
30953 editor
30954 .update(cx, |editor, window, cx| {
30955 editor.submit_diff_review_comment(window, cx);
30956 })
30957 .unwrap();
30958
30959 // Verify no comment was added
30960 editor
30961 .update(cx, |editor, _window, _cx| {
30962 assert_eq!(editor.total_review_comment_count(), 0);
30963 })
30964 .unwrap();
30965
30966 // Try to submit with whitespace-only comment
30967 editor
30968 .update(cx, |editor, window, cx| {
30969 if let Some(prompt_editor) = editor.diff_review_prompt_editor().cloned() {
30970 prompt_editor.update(cx, |pe, cx| {
30971 pe.insert(" \n\t ", window, cx);
30972 });
30973 }
30974 editor.submit_diff_review_comment(window, cx);
30975 })
30976 .unwrap();
30977
30978 // Verify still no comment was added
30979 editor
30980 .update(cx, |editor, _window, _cx| {
30981 assert_eq!(editor.total_review_comment_count(), 0);
30982 })
30983 .unwrap();
30984}
30985
30986#[gpui::test]
30987fn test_diff_review_inline_edit_flow(cx: &mut TestAppContext) {
30988 init_test(cx, |_| {});
30989
30990 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30991
30992 // Add a comment directly
30993 let comment_id = editor
30994 .update(cx, |editor, _window, cx| {
30995 let key = test_hunk_key("");
30996 add_test_comment(editor, key, "Original comment", cx)
30997 })
30998 .unwrap();
30999
31000 // Set comment to editing mode
31001 editor
31002 .update(cx, |editor, _window, cx| {
31003 editor.set_comment_editing(comment_id, true, cx);
31004 })
31005 .unwrap();
31006
31007 // Verify editing flag is set
31008 editor
31009 .update(cx, |editor, _window, cx| {
31010 let key = test_hunk_key("");
31011 let snapshot = editor.buffer().read(cx).snapshot(cx);
31012 let comments = editor.comments_for_hunk(&key, &snapshot);
31013 assert_eq!(comments.len(), 1);
31014 assert!(comments[0].is_editing);
31015 })
31016 .unwrap();
31017
31018 // Update the comment
31019 editor
31020 .update(cx, |editor, _window, cx| {
31021 let updated =
31022 editor.update_review_comment(comment_id, "Updated comment".to_string(), cx);
31023 assert!(updated);
31024 })
31025 .unwrap();
31026
31027 // Verify comment was updated and editing flag is cleared
31028 editor
31029 .update(cx, |editor, _window, cx| {
31030 let key = test_hunk_key("");
31031 let snapshot = editor.buffer().read(cx).snapshot(cx);
31032 let comments = editor.comments_for_hunk(&key, &snapshot);
31033 assert_eq!(comments[0].comment, "Updated comment");
31034 assert!(!comments[0].is_editing);
31035 })
31036 .unwrap();
31037}
31038
31039#[gpui::test]
31040fn test_orphaned_comments_are_cleaned_up(cx: &mut TestAppContext) {
31041 init_test(cx, |_| {});
31042
31043 // Create an editor with some text
31044 let editor = cx.add_window(|window, cx| {
31045 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
31046 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31047 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
31048 });
31049
31050 // Add a comment with an anchor on line 2
31051 editor
31052 .update(cx, |editor, _window, cx| {
31053 let snapshot = editor.buffer().read(cx).snapshot(cx);
31054 let anchor = snapshot.anchor_after(Point::new(1, 0)); // Line 2
31055 let key = DiffHunkKey {
31056 file_path: Arc::from(util::rel_path::RelPath::empty()),
31057 hunk_start_anchor: anchor,
31058 };
31059 editor.add_review_comment(key, "Comment on line 2".to_string(), anchor..anchor, cx);
31060 assert_eq!(editor.total_review_comment_count(), 1);
31061 })
31062 .unwrap();
31063
31064 // Delete all content (this should orphan the comment's anchor)
31065 editor
31066 .update(cx, |editor, window, cx| {
31067 editor.select_all(&SelectAll, window, cx);
31068 editor.insert("completely new content", window, cx);
31069 })
31070 .unwrap();
31071
31072 // Trigger cleanup
31073 editor
31074 .update(cx, |editor, _window, cx| {
31075 editor.cleanup_orphaned_review_comments(cx);
31076 // Comment should be removed because its anchor is invalid
31077 assert_eq!(editor.total_review_comment_count(), 0);
31078 })
31079 .unwrap();
31080}
31081
31082#[gpui::test]
31083fn test_orphaned_comments_cleanup_called_on_buffer_edit(cx: &mut TestAppContext) {
31084 init_test(cx, |_| {});
31085
31086 // Create an editor with some text
31087 let editor = cx.add_window(|window, cx| {
31088 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
31089 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31090 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
31091 });
31092
31093 // Add a comment with an anchor on line 2
31094 editor
31095 .update(cx, |editor, _window, cx| {
31096 let snapshot = editor.buffer().read(cx).snapshot(cx);
31097 let anchor = snapshot.anchor_after(Point::new(1, 0)); // Line 2
31098 let key = DiffHunkKey {
31099 file_path: Arc::from(util::rel_path::RelPath::empty()),
31100 hunk_start_anchor: anchor,
31101 };
31102 editor.add_review_comment(key, "Comment on line 2".to_string(), anchor..anchor, cx);
31103 assert_eq!(editor.total_review_comment_count(), 1);
31104 })
31105 .unwrap();
31106
31107 // Edit the buffer - this should trigger cleanup via on_buffer_event
31108 // Delete all content which orphans the anchor
31109 editor
31110 .update(cx, |editor, window, cx| {
31111 editor.select_all(&SelectAll, window, cx);
31112 editor.insert("completely new content", window, cx);
31113 // The cleanup is called automatically in on_buffer_event when Edited fires
31114 })
31115 .unwrap();
31116
31117 // Verify cleanup happened automatically (not manually triggered)
31118 editor
31119 .update(cx, |editor, _window, _cx| {
31120 // Comment should be removed because its anchor became invalid
31121 // and cleanup was called automatically on buffer edit
31122 assert_eq!(editor.total_review_comment_count(), 0);
31123 })
31124 .unwrap();
31125}
31126
31127#[gpui::test]
31128fn test_comments_stored_for_multiple_hunks(cx: &mut TestAppContext) {
31129 init_test(cx, |_| {});
31130
31131 // This test verifies that comments can be stored for multiple different hunks
31132 // and that hunk_comment_count correctly identifies comments per hunk.
31133 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31134
31135 _ = editor.update(cx, |editor, _window, cx| {
31136 let snapshot = editor.buffer().read(cx).snapshot(cx);
31137
31138 // Create two different hunk keys (simulating two different files)
31139 let anchor = snapshot.anchor_before(Point::new(0, 0));
31140 let key1 = DiffHunkKey {
31141 file_path: Arc::from(util::rel_path::RelPath::unix("file1.rs").unwrap()),
31142 hunk_start_anchor: anchor,
31143 };
31144 let key2 = DiffHunkKey {
31145 file_path: Arc::from(util::rel_path::RelPath::unix("file2.rs").unwrap()),
31146 hunk_start_anchor: anchor,
31147 };
31148
31149 // Add comments to first hunk
31150 editor.add_review_comment(
31151 key1.clone(),
31152 "Comment 1 for file1".to_string(),
31153 anchor..anchor,
31154 cx,
31155 );
31156 editor.add_review_comment(
31157 key1.clone(),
31158 "Comment 2 for file1".to_string(),
31159 anchor..anchor,
31160 cx,
31161 );
31162
31163 // Add comment to second hunk
31164 editor.add_review_comment(
31165 key2.clone(),
31166 "Comment for file2".to_string(),
31167 anchor..anchor,
31168 cx,
31169 );
31170
31171 // Verify total count
31172 assert_eq!(editor.total_review_comment_count(), 3);
31173
31174 // Verify per-hunk counts
31175 let snapshot = editor.buffer().read(cx).snapshot(cx);
31176 assert_eq!(
31177 editor.hunk_comment_count(&key1, &snapshot),
31178 2,
31179 "file1 should have 2 comments"
31180 );
31181 assert_eq!(
31182 editor.hunk_comment_count(&key2, &snapshot),
31183 1,
31184 "file2 should have 1 comment"
31185 );
31186
31187 // Verify comments_for_hunk returns correct comments
31188 let file1_comments = editor.comments_for_hunk(&key1, &snapshot);
31189 assert_eq!(file1_comments.len(), 2);
31190 assert_eq!(file1_comments[0].comment, "Comment 1 for file1");
31191 assert_eq!(file1_comments[1].comment, "Comment 2 for file1");
31192
31193 let file2_comments = editor.comments_for_hunk(&key2, &snapshot);
31194 assert_eq!(file2_comments.len(), 1);
31195 assert_eq!(file2_comments[0].comment, "Comment for file2");
31196 });
31197}
31198
31199#[gpui::test]
31200fn test_same_hunk_detected_by_matching_keys(cx: &mut TestAppContext) {
31201 init_test(cx, |_| {});
31202
31203 // This test verifies that hunk_keys_match correctly identifies when two
31204 // DiffHunkKeys refer to the same hunk (same file path and anchor point).
31205 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31206
31207 _ = editor.update(cx, |editor, _window, cx| {
31208 let snapshot = editor.buffer().read(cx).snapshot(cx);
31209 let anchor = snapshot.anchor_before(Point::new(0, 0));
31210
31211 // Create two keys with the same file path and anchor
31212 let key1 = DiffHunkKey {
31213 file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
31214 hunk_start_anchor: anchor,
31215 };
31216 let key2 = DiffHunkKey {
31217 file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
31218 hunk_start_anchor: anchor,
31219 };
31220
31221 // Add comment to first key
31222 editor.add_review_comment(key1, "Test comment".to_string(), anchor..anchor, cx);
31223
31224 // Verify second key (same hunk) finds the comment
31225 let snapshot = editor.buffer().read(cx).snapshot(cx);
31226 assert_eq!(
31227 editor.hunk_comment_count(&key2, &snapshot),
31228 1,
31229 "Same hunk should find the comment"
31230 );
31231
31232 // Create a key with different file path
31233 let different_file_key = DiffHunkKey {
31234 file_path: Arc::from(util::rel_path::RelPath::unix("other.rs").unwrap()),
31235 hunk_start_anchor: anchor,
31236 };
31237
31238 // Different file should not find the comment
31239 assert_eq!(
31240 editor.hunk_comment_count(&different_file_key, &snapshot),
31241 0,
31242 "Different file should not find the comment"
31243 );
31244 });
31245}
31246
31247#[gpui::test]
31248fn test_overlay_comments_expanded_state(cx: &mut TestAppContext) {
31249 init_test(cx, |_| {});
31250
31251 // This test verifies that set_diff_review_comments_expanded correctly
31252 // updates the expanded state of overlays.
31253 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31254
31255 // Show overlay
31256 editor
31257 .update(cx, |editor, window, cx| {
31258 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
31259 })
31260 .unwrap();
31261
31262 // Verify initially expanded (default)
31263 editor
31264 .update(cx, |editor, _window, _cx| {
31265 assert!(
31266 editor.diff_review_overlays[0].comments_expanded,
31267 "Should be expanded by default"
31268 );
31269 })
31270 .unwrap();
31271
31272 // Set to collapsed using the public method
31273 editor
31274 .update(cx, |editor, _window, cx| {
31275 editor.set_diff_review_comments_expanded(false, cx);
31276 })
31277 .unwrap();
31278
31279 // Verify collapsed
31280 editor
31281 .update(cx, |editor, _window, _cx| {
31282 assert!(
31283 !editor.diff_review_overlays[0].comments_expanded,
31284 "Should be collapsed after setting to false"
31285 );
31286 })
31287 .unwrap();
31288
31289 // Set back to expanded
31290 editor
31291 .update(cx, |editor, _window, cx| {
31292 editor.set_diff_review_comments_expanded(true, cx);
31293 })
31294 .unwrap();
31295
31296 // Verify expanded again
31297 editor
31298 .update(cx, |editor, _window, _cx| {
31299 assert!(
31300 editor.diff_review_overlays[0].comments_expanded,
31301 "Should be expanded after setting to true"
31302 );
31303 })
31304 .unwrap();
31305}
31306
31307#[gpui::test]
31308fn test_diff_review_multiline_selection(cx: &mut TestAppContext) {
31309 init_test(cx, |_| {});
31310
31311 // Create an editor with multiple lines of text
31312 let editor = cx.add_window(|window, cx| {
31313 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\nline 4\nline 5\n", cx));
31314 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31315 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
31316 });
31317
31318 // Test showing overlay with a multi-line selection (lines 1-3, which are rows 0-2)
31319 editor
31320 .update(cx, |editor, window, cx| {
31321 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(2), window, cx);
31322 })
31323 .unwrap();
31324
31325 // Verify line range
31326 editor
31327 .update(cx, |editor, _window, cx| {
31328 assert!(!editor.diff_review_overlays.is_empty());
31329 assert_eq!(editor.diff_review_line_range(cx), Some((0, 2)));
31330 })
31331 .unwrap();
31332
31333 // Dismiss and test with reversed range (end < start)
31334 editor
31335 .update(cx, |editor, _window, cx| {
31336 editor.dismiss_all_diff_review_overlays(cx);
31337 })
31338 .unwrap();
31339
31340 // Show overlay with reversed range - should normalize it
31341 editor
31342 .update(cx, |editor, window, cx| {
31343 editor.show_diff_review_overlay(DisplayRow(3)..DisplayRow(1), window, cx);
31344 })
31345 .unwrap();
31346
31347 // Verify range is normalized (start <= end)
31348 editor
31349 .update(cx, |editor, _window, cx| {
31350 assert_eq!(editor.diff_review_line_range(cx), Some((1, 3)));
31351 })
31352 .unwrap();
31353}
31354
31355#[gpui::test]
31356fn test_diff_review_drag_state(cx: &mut TestAppContext) {
31357 init_test(cx, |_| {});
31358
31359 let editor = cx.add_window(|window, cx| {
31360 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
31361 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31362 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
31363 });
31364
31365 // Initially no drag state
31366 editor
31367 .update(cx, |editor, _window, _cx| {
31368 assert!(editor.diff_review_drag_state.is_none());
31369 })
31370 .unwrap();
31371
31372 // Start drag at row 1
31373 editor
31374 .update(cx, |editor, window, cx| {
31375 editor.start_diff_review_drag(DisplayRow(1), window, cx);
31376 })
31377 .unwrap();
31378
31379 // Verify drag state is set
31380 editor
31381 .update(cx, |editor, window, cx| {
31382 assert!(editor.diff_review_drag_state.is_some());
31383 let snapshot = editor.snapshot(window, cx);
31384 let range = editor
31385 .diff_review_drag_state
31386 .as_ref()
31387 .unwrap()
31388 .row_range(&snapshot.display_snapshot);
31389 assert_eq!(*range.start(), DisplayRow(1));
31390 assert_eq!(*range.end(), DisplayRow(1));
31391 })
31392 .unwrap();
31393
31394 // Update drag to row 3
31395 editor
31396 .update(cx, |editor, window, cx| {
31397 editor.update_diff_review_drag(DisplayRow(3), window, cx);
31398 })
31399 .unwrap();
31400
31401 // Verify drag state is updated
31402 editor
31403 .update(cx, |editor, window, cx| {
31404 assert!(editor.diff_review_drag_state.is_some());
31405 let snapshot = editor.snapshot(window, cx);
31406 let range = editor
31407 .diff_review_drag_state
31408 .as_ref()
31409 .unwrap()
31410 .row_range(&snapshot.display_snapshot);
31411 assert_eq!(*range.start(), DisplayRow(1));
31412 assert_eq!(*range.end(), DisplayRow(3));
31413 })
31414 .unwrap();
31415
31416 // End drag - should show overlay
31417 editor
31418 .update(cx, |editor, window, cx| {
31419 editor.end_diff_review_drag(window, cx);
31420 })
31421 .unwrap();
31422
31423 // Verify drag state is cleared and overlay is shown
31424 editor
31425 .update(cx, |editor, _window, cx| {
31426 assert!(editor.diff_review_drag_state.is_none());
31427 assert!(!editor.diff_review_overlays.is_empty());
31428 assert_eq!(editor.diff_review_line_range(cx), Some((1, 3)));
31429 })
31430 .unwrap();
31431}
31432
31433#[gpui::test]
31434fn test_diff_review_drag_cancel(cx: &mut TestAppContext) {
31435 init_test(cx, |_| {});
31436
31437 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31438
31439 // Start drag
31440 editor
31441 .update(cx, |editor, window, cx| {
31442 editor.start_diff_review_drag(DisplayRow(0), window, cx);
31443 })
31444 .unwrap();
31445
31446 // Verify drag state is set
31447 editor
31448 .update(cx, |editor, _window, _cx| {
31449 assert!(editor.diff_review_drag_state.is_some());
31450 })
31451 .unwrap();
31452
31453 // Cancel drag
31454 editor
31455 .update(cx, |editor, _window, cx| {
31456 editor.cancel_diff_review_drag(cx);
31457 })
31458 .unwrap();
31459
31460 // Verify drag state is cleared and no overlay was created
31461 editor
31462 .update(cx, |editor, _window, _cx| {
31463 assert!(editor.diff_review_drag_state.is_none());
31464 assert!(editor.diff_review_overlays.is_empty());
31465 })
31466 .unwrap();
31467}
31468
31469#[gpui::test]
31470fn test_calculate_overlay_height(cx: &mut TestAppContext) {
31471 init_test(cx, |_| {});
31472
31473 // This test verifies that calculate_overlay_height returns correct heights
31474 // based on comment count and expanded state.
31475 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31476
31477 _ = editor.update(cx, |editor, _window, cx| {
31478 let snapshot = editor.buffer().read(cx).snapshot(cx);
31479 let anchor = snapshot.anchor_before(Point::new(0, 0));
31480 let key = DiffHunkKey {
31481 file_path: Arc::from(util::rel_path::RelPath::empty()),
31482 hunk_start_anchor: anchor,
31483 };
31484
31485 // No comments: base height of 2
31486 let height_no_comments = editor.calculate_overlay_height(&key, true, &snapshot);
31487 assert_eq!(
31488 height_no_comments, 2,
31489 "Base height should be 2 with no comments"
31490 );
31491
31492 // Add one comment
31493 editor.add_review_comment(key.clone(), "Comment 1".to_string(), anchor..anchor, cx);
31494
31495 let snapshot = editor.buffer().read(cx).snapshot(cx);
31496
31497 // With comments expanded: base (2) + header (1) + 2 per comment
31498 let height_expanded = editor.calculate_overlay_height(&key, true, &snapshot);
31499 assert_eq!(
31500 height_expanded,
31501 2 + 1 + 2, // base + header + 1 comment * 2
31502 "Height with 1 comment expanded"
31503 );
31504
31505 // With comments collapsed: base (2) + header (1)
31506 let height_collapsed = editor.calculate_overlay_height(&key, false, &snapshot);
31507 assert_eq!(
31508 height_collapsed,
31509 2 + 1, // base + header only
31510 "Height with comments collapsed"
31511 );
31512
31513 // Add more comments
31514 editor.add_review_comment(key.clone(), "Comment 2".to_string(), anchor..anchor, cx);
31515 editor.add_review_comment(key.clone(), "Comment 3".to_string(), anchor..anchor, cx);
31516
31517 let snapshot = editor.buffer().read(cx).snapshot(cx);
31518
31519 // With 3 comments expanded
31520 let height_3_expanded = editor.calculate_overlay_height(&key, true, &snapshot);
31521 assert_eq!(
31522 height_3_expanded,
31523 2 + 1 + (3 * 2), // base + header + 3 comments * 2
31524 "Height with 3 comments expanded"
31525 );
31526
31527 // Collapsed height stays the same regardless of comment count
31528 let height_3_collapsed = editor.calculate_overlay_height(&key, false, &snapshot);
31529 assert_eq!(
31530 height_3_collapsed,
31531 2 + 1, // base + header only
31532 "Height with 3 comments collapsed should be same as 1 comment collapsed"
31533 );
31534 });
31535}
31536
31537#[gpui::test]
31538async fn test_move_to_start_end_of_larger_syntax_node_single_cursor(cx: &mut TestAppContext) {
31539 init_test(cx, |_| {});
31540
31541 let language = Arc::new(Language::new(
31542 LanguageConfig::default(),
31543 Some(tree_sitter_rust::LANGUAGE.into()),
31544 ));
31545
31546 let text = r#"
31547 fn main() {
31548 let x = foo(1, 2);
31549 }
31550 "#
31551 .unindent();
31552
31553 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
31554 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31555 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
31556
31557 editor
31558 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
31559 .await;
31560
31561 // Test case 1: Move to end of syntax nodes
31562 editor.update_in(cx, |editor, window, cx| {
31563 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31564 s.select_display_ranges([
31565 DisplayPoint::new(DisplayRow(1), 16)..DisplayPoint::new(DisplayRow(1), 16)
31566 ]);
31567 });
31568 });
31569 editor.update(cx, |editor, cx| {
31570 assert_text_with_selections(
31571 editor,
31572 indoc! {r#"
31573 fn main() {
31574 let x = foo(ˇ1, 2);
31575 }
31576 "#},
31577 cx,
31578 );
31579 });
31580 editor.update_in(cx, |editor, window, cx| {
31581 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31582 });
31583 editor.update(cx, |editor, cx| {
31584 assert_text_with_selections(
31585 editor,
31586 indoc! {r#"
31587 fn main() {
31588 let x = foo(1ˇ, 2);
31589 }
31590 "#},
31591 cx,
31592 );
31593 });
31594 editor.update_in(cx, |editor, window, cx| {
31595 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31596 });
31597 editor.update(cx, |editor, cx| {
31598 assert_text_with_selections(
31599 editor,
31600 indoc! {r#"
31601 fn main() {
31602 let x = foo(1, 2)ˇ;
31603 }
31604 "#},
31605 cx,
31606 );
31607 });
31608 editor.update_in(cx, |editor, window, cx| {
31609 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31610 });
31611 editor.update(cx, |editor, cx| {
31612 assert_text_with_selections(
31613 editor,
31614 indoc! {r#"
31615 fn main() {
31616 let x = foo(1, 2);ˇ
31617 }
31618 "#},
31619 cx,
31620 );
31621 });
31622 editor.update_in(cx, |editor, window, cx| {
31623 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31624 });
31625 editor.update(cx, |editor, cx| {
31626 assert_text_with_selections(
31627 editor,
31628 indoc! {r#"
31629 fn main() {
31630 let x = foo(1, 2);
31631 }ˇ
31632 "#},
31633 cx,
31634 );
31635 });
31636
31637 // Test case 2: Move to start of syntax nodes
31638 editor.update_in(cx, |editor, window, cx| {
31639 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31640 s.select_display_ranges([
31641 DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20)
31642 ]);
31643 });
31644 });
31645 editor.update(cx, |editor, cx| {
31646 assert_text_with_selections(
31647 editor,
31648 indoc! {r#"
31649 fn main() {
31650 let x = foo(1, 2ˇ);
31651 }
31652 "#},
31653 cx,
31654 );
31655 });
31656 editor.update_in(cx, |editor, window, cx| {
31657 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31658 });
31659 editor.update(cx, |editor, cx| {
31660 assert_text_with_selections(
31661 editor,
31662 indoc! {r#"
31663 fn main() {
31664 let x = fooˇ(1, 2);
31665 }
31666 "#},
31667 cx,
31668 );
31669 });
31670 editor.update_in(cx, |editor, window, cx| {
31671 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31672 });
31673 editor.update(cx, |editor, cx| {
31674 assert_text_with_selections(
31675 editor,
31676 indoc! {r#"
31677 fn main() {
31678 let x = ˇfoo(1, 2);
31679 }
31680 "#},
31681 cx,
31682 );
31683 });
31684 editor.update_in(cx, |editor, window, cx| {
31685 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31686 });
31687 editor.update(cx, |editor, cx| {
31688 assert_text_with_selections(
31689 editor,
31690 indoc! {r#"
31691 fn main() {
31692 ˇlet x = foo(1, 2);
31693 }
31694 "#},
31695 cx,
31696 );
31697 });
31698 editor.update_in(cx, |editor, window, cx| {
31699 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31700 });
31701 editor.update(cx, |editor, cx| {
31702 assert_text_with_selections(
31703 editor,
31704 indoc! {r#"
31705 fn main() ˇ{
31706 let x = foo(1, 2);
31707 }
31708 "#},
31709 cx,
31710 );
31711 });
31712 editor.update_in(cx, |editor, window, cx| {
31713 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31714 });
31715 editor.update(cx, |editor, cx| {
31716 assert_text_with_selections(
31717 editor,
31718 indoc! {r#"
31719 ˇfn main() {
31720 let x = foo(1, 2);
31721 }
31722 "#},
31723 cx,
31724 );
31725 });
31726}
31727
31728#[gpui::test]
31729async fn test_move_to_start_end_of_larger_syntax_node_two_cursors(cx: &mut TestAppContext) {
31730 init_test(cx, |_| {});
31731
31732 let language = Arc::new(Language::new(
31733 LanguageConfig::default(),
31734 Some(tree_sitter_rust::LANGUAGE.into()),
31735 ));
31736
31737 let text = r#"
31738 fn main() {
31739 let x = foo(1, 2);
31740 let y = bar(3, 4);
31741 }
31742 "#
31743 .unindent();
31744
31745 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
31746 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31747 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
31748
31749 editor
31750 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
31751 .await;
31752
31753 // Test case 1: Move to end of syntax nodes with two cursors
31754 editor.update_in(cx, |editor, window, cx| {
31755 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31756 s.select_display_ranges([
31757 DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20),
31758 DisplayPoint::new(DisplayRow(2), 20)..DisplayPoint::new(DisplayRow(2), 20),
31759 ]);
31760 });
31761 });
31762 editor.update(cx, |editor, cx| {
31763 assert_text_with_selections(
31764 editor,
31765 indoc! {r#"
31766 fn main() {
31767 let x = foo(1, 2ˇ);
31768 let y = bar(3, 4ˇ);
31769 }
31770 "#},
31771 cx,
31772 );
31773 });
31774 editor.update_in(cx, |editor, window, cx| {
31775 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31776 });
31777 editor.update(cx, |editor, cx| {
31778 assert_text_with_selections(
31779 editor,
31780 indoc! {r#"
31781 fn main() {
31782 let x = foo(1, 2)ˇ;
31783 let y = bar(3, 4)ˇ;
31784 }
31785 "#},
31786 cx,
31787 );
31788 });
31789 editor.update_in(cx, |editor, window, cx| {
31790 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31791 });
31792 editor.update(cx, |editor, cx| {
31793 assert_text_with_selections(
31794 editor,
31795 indoc! {r#"
31796 fn main() {
31797 let x = foo(1, 2);ˇ
31798 let y = bar(3, 4);ˇ
31799 }
31800 "#},
31801 cx,
31802 );
31803 });
31804
31805 // Test case 2: Move to start of syntax nodes with two cursors
31806 editor.update_in(cx, |editor, window, cx| {
31807 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31808 s.select_display_ranges([
31809 DisplayPoint::new(DisplayRow(1), 19)..DisplayPoint::new(DisplayRow(1), 19),
31810 DisplayPoint::new(DisplayRow(2), 19)..DisplayPoint::new(DisplayRow(2), 19),
31811 ]);
31812 });
31813 });
31814 editor.update(cx, |editor, cx| {
31815 assert_text_with_selections(
31816 editor,
31817 indoc! {r#"
31818 fn main() {
31819 let x = foo(1, ˇ2);
31820 let y = bar(3, ˇ4);
31821 }
31822 "#},
31823 cx,
31824 );
31825 });
31826 editor.update_in(cx, |editor, window, cx| {
31827 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31828 });
31829 editor.update(cx, |editor, cx| {
31830 assert_text_with_selections(
31831 editor,
31832 indoc! {r#"
31833 fn main() {
31834 let x = fooˇ(1, 2);
31835 let y = barˇ(3, 4);
31836 }
31837 "#},
31838 cx,
31839 );
31840 });
31841 editor.update_in(cx, |editor, window, cx| {
31842 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31843 });
31844 editor.update(cx, |editor, cx| {
31845 assert_text_with_selections(
31846 editor,
31847 indoc! {r#"
31848 fn main() {
31849 let x = ˇfoo(1, 2);
31850 let y = ˇbar(3, 4);
31851 }
31852 "#},
31853 cx,
31854 );
31855 });
31856 editor.update_in(cx, |editor, window, cx| {
31857 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31858 });
31859 editor.update(cx, |editor, cx| {
31860 assert_text_with_selections(
31861 editor,
31862 indoc! {r#"
31863 fn main() {
31864 ˇlet x = foo(1, 2);
31865 ˇlet y = bar(3, 4);
31866 }
31867 "#},
31868 cx,
31869 );
31870 });
31871}
31872
31873#[gpui::test]
31874async fn test_move_to_start_end_of_larger_syntax_node_with_selections_and_strings(
31875 cx: &mut TestAppContext,
31876) {
31877 init_test(cx, |_| {});
31878
31879 let language = Arc::new(Language::new(
31880 LanguageConfig::default(),
31881 Some(tree_sitter_rust::LANGUAGE.into()),
31882 ));
31883
31884 let text = r#"
31885 fn main() {
31886 let x = foo(1, 2);
31887 let msg = "hello world";
31888 }
31889 "#
31890 .unindent();
31891
31892 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
31893 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31894 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
31895
31896 editor
31897 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
31898 .await;
31899
31900 // Test case 1: With existing selection, move_to_end keeps selection
31901 editor.update_in(cx, |editor, window, cx| {
31902 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31903 s.select_display_ranges([
31904 DisplayPoint::new(DisplayRow(1), 12)..DisplayPoint::new(DisplayRow(1), 21)
31905 ]);
31906 });
31907 });
31908 editor.update(cx, |editor, cx| {
31909 assert_text_with_selections(
31910 editor,
31911 indoc! {r#"
31912 fn main() {
31913 let x = «foo(1, 2)ˇ»;
31914 let msg = "hello world";
31915 }
31916 "#},
31917 cx,
31918 );
31919 });
31920 editor.update_in(cx, |editor, window, cx| {
31921 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31922 });
31923 editor.update(cx, |editor, cx| {
31924 assert_text_with_selections(
31925 editor,
31926 indoc! {r#"
31927 fn main() {
31928 let x = «foo(1, 2)ˇ»;
31929 let msg = "hello world";
31930 }
31931 "#},
31932 cx,
31933 );
31934 });
31935
31936 // Test case 2: Move to end within a string
31937 editor.update_in(cx, |editor, window, cx| {
31938 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31939 s.select_display_ranges([
31940 DisplayPoint::new(DisplayRow(2), 15)..DisplayPoint::new(DisplayRow(2), 15)
31941 ]);
31942 });
31943 });
31944 editor.update(cx, |editor, cx| {
31945 assert_text_with_selections(
31946 editor,
31947 indoc! {r#"
31948 fn main() {
31949 let x = foo(1, 2);
31950 let msg = "ˇhello world";
31951 }
31952 "#},
31953 cx,
31954 );
31955 });
31956 editor.update_in(cx, |editor, window, cx| {
31957 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31958 });
31959 editor.update(cx, |editor, cx| {
31960 assert_text_with_selections(
31961 editor,
31962 indoc! {r#"
31963 fn main() {
31964 let x = foo(1, 2);
31965 let msg = "hello worldˇ";
31966 }
31967 "#},
31968 cx,
31969 );
31970 });
31971
31972 // Test case 3: Move to start within a string
31973 editor.update_in(cx, |editor, window, cx| {
31974 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31975 s.select_display_ranges([
31976 DisplayPoint::new(DisplayRow(2), 21)..DisplayPoint::new(DisplayRow(2), 21)
31977 ]);
31978 });
31979 });
31980 editor.update(cx, |editor, cx| {
31981 assert_text_with_selections(
31982 editor,
31983 indoc! {r#"
31984 fn main() {
31985 let x = foo(1, 2);
31986 let msg = "hello ˇworld";
31987 }
31988 "#},
31989 cx,
31990 );
31991 });
31992 editor.update_in(cx, |editor, window, cx| {
31993 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31994 });
31995 editor.update(cx, |editor, cx| {
31996 assert_text_with_selections(
31997 editor,
31998 indoc! {r#"
31999 fn main() {
32000 let x = foo(1, 2);
32001 let msg = "ˇhello world";
32002 }
32003 "#},
32004 cx,
32005 );
32006 });
32007}
32008
32009#[gpui::test]
32010async fn test_select_to_start_end_of_larger_syntax_node(cx: &mut TestAppContext) {
32011 init_test(cx, |_| {});
32012
32013 let language = Arc::new(Language::new(
32014 LanguageConfig::default(),
32015 Some(tree_sitter_rust::LANGUAGE.into()),
32016 ));
32017
32018 // Test Group 1.1: Cursor in String - First Jump (Select to End)
32019 let text = r#"let msg = "foo bar baz";"#.unindent();
32020
32021 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32022 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32023 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32024
32025 editor
32026 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32027 .await;
32028
32029 editor.update_in(cx, |editor, window, cx| {
32030 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32031 s.select_display_ranges([
32032 DisplayPoint::new(DisplayRow(0), 14)..DisplayPoint::new(DisplayRow(0), 14)
32033 ]);
32034 });
32035 });
32036 editor.update(cx, |editor, cx| {
32037 assert_text_with_selections(editor, indoc! {r#"let msg = "fooˇ bar baz";"#}, cx);
32038 });
32039 editor.update_in(cx, |editor, window, cx| {
32040 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32041 });
32042 editor.update(cx, |editor, cx| {
32043 assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar bazˇ»";"#}, cx);
32044 });
32045
32046 // Test Group 1.2: Cursor in String - Second Jump (Select to End)
32047 editor.update_in(cx, |editor, window, cx| {
32048 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32049 });
32050 editor.update(cx, |editor, cx| {
32051 assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar baz"ˇ»;"#}, cx);
32052 });
32053
32054 // Test Group 1.3: Cursor in String - Third Jump (Select to End)
32055 editor.update_in(cx, |editor, window, cx| {
32056 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32057 });
32058 editor.update(cx, |editor, cx| {
32059 assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar baz";ˇ»"#}, cx);
32060 });
32061
32062 // Test Group 1.4: Cursor in String - First Jump (Select to Start)
32063 editor.update_in(cx, |editor, window, cx| {
32064 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32065 s.select_display_ranges([
32066 DisplayPoint::new(DisplayRow(0), 18)..DisplayPoint::new(DisplayRow(0), 18)
32067 ]);
32068 });
32069 });
32070 editor.update(cx, |editor, cx| {
32071 assert_text_with_selections(editor, indoc! {r#"let msg = "foo barˇ baz";"#}, cx);
32072 });
32073 editor.update_in(cx, |editor, window, cx| {
32074 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
32075 });
32076 editor.update(cx, |editor, cx| {
32077 assert_text_with_selections(editor, indoc! {r#"let msg = "«ˇfoo bar» baz";"#}, cx);
32078 });
32079
32080 // Test Group 1.5: Cursor in String - Second Jump (Select to Start)
32081 editor.update_in(cx, |editor, window, cx| {
32082 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
32083 });
32084 editor.update(cx, |editor, cx| {
32085 assert_text_with_selections(editor, indoc! {r#"let msg = «ˇ"foo bar» baz";"#}, cx);
32086 });
32087
32088 // Test Group 1.6: Cursor in String - Third Jump (Select to Start)
32089 editor.update_in(cx, |editor, window, cx| {
32090 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
32091 });
32092 editor.update(cx, |editor, cx| {
32093 assert_text_with_selections(editor, indoc! {r#"«ˇlet msg = "foo bar» baz";"#}, cx);
32094 });
32095
32096 // Test Group 2.1: Let Statement Progression (Select to End)
32097 let text = r#"
32098fn main() {
32099 let x = "hello";
32100}
32101"#
32102 .unindent();
32103
32104 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32105 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32106 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32107
32108 editor
32109 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32110 .await;
32111
32112 editor.update_in(cx, |editor, window, cx| {
32113 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32114 s.select_display_ranges([
32115 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)
32116 ]);
32117 });
32118 });
32119 editor.update(cx, |editor, cx| {
32120 assert_text_with_selections(
32121 editor,
32122 indoc! {r#"
32123 fn main() {
32124 let xˇ = "hello";
32125 }
32126 "#},
32127 cx,
32128 );
32129 });
32130 editor.update_in(cx, |editor, window, cx| {
32131 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32132 });
32133 editor.update(cx, |editor, cx| {
32134 assert_text_with_selections(
32135 editor,
32136 indoc! {r##"
32137 fn main() {
32138 let x« = "hello";ˇ»
32139 }
32140 "##},
32141 cx,
32142 );
32143 });
32144 editor.update_in(cx, |editor, window, cx| {
32145 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32146 });
32147 editor.update(cx, |editor, cx| {
32148 assert_text_with_selections(
32149 editor,
32150 indoc! {r#"
32151 fn main() {
32152 let x« = "hello";
32153 }ˇ»
32154 "#},
32155 cx,
32156 );
32157 });
32158
32159 // Test Group 2.2a: From Inside String Content Node To String Content Boundary
32160 let text = r#"let x = "hello";"#.unindent();
32161
32162 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32163 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32164 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32165
32166 editor
32167 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32168 .await;
32169
32170 editor.update_in(cx, |editor, window, cx| {
32171 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32172 s.select_display_ranges([
32173 DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12)
32174 ]);
32175 });
32176 });
32177 editor.update(cx, |editor, cx| {
32178 assert_text_with_selections(editor, indoc! {r#"let x = "helˇlo";"#}, cx);
32179 });
32180 editor.update_in(cx, |editor, window, cx| {
32181 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
32182 });
32183 editor.update(cx, |editor, cx| {
32184 assert_text_with_selections(editor, indoc! {r#"let x = "«ˇhel»lo";"#}, cx);
32185 });
32186
32187 // Test Group 2.2b: From Edge of String Content Node To String Literal Boundary
32188 editor.update_in(cx, |editor, window, cx| {
32189 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32190 s.select_display_ranges([
32191 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
32192 ]);
32193 });
32194 });
32195 editor.update(cx, |editor, cx| {
32196 assert_text_with_selections(editor, indoc! {r#"let x = "ˇhello";"#}, cx);
32197 });
32198 editor.update_in(cx, |editor, window, cx| {
32199 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
32200 });
32201 editor.update(cx, |editor, cx| {
32202 assert_text_with_selections(editor, indoc! {r#"let x = «ˇ"»hello";"#}, cx);
32203 });
32204
32205 // Test Group 3.1: Create Selection from Cursor (Select to End)
32206 let text = r#"let x = "hello world";"#.unindent();
32207
32208 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32209 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32210 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32211
32212 editor
32213 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32214 .await;
32215
32216 editor.update_in(cx, |editor, window, cx| {
32217 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32218 s.select_display_ranges([
32219 DisplayPoint::new(DisplayRow(0), 14)..DisplayPoint::new(DisplayRow(0), 14)
32220 ]);
32221 });
32222 });
32223 editor.update(cx, |editor, cx| {
32224 assert_text_with_selections(editor, indoc! {r#"let x = "helloˇ world";"#}, cx);
32225 });
32226 editor.update_in(cx, |editor, window, cx| {
32227 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32228 });
32229 editor.update(cx, |editor, cx| {
32230 assert_text_with_selections(editor, indoc! {r#"let x = "hello« worldˇ»";"#}, cx);
32231 });
32232
32233 // Test Group 3.2: Extend Existing Selection (Select to End)
32234 editor.update_in(cx, |editor, window, cx| {
32235 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32236 s.select_display_ranges([
32237 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 17)
32238 ]);
32239 });
32240 });
32241 editor.update(cx, |editor, cx| {
32242 assert_text_with_selections(editor, indoc! {r#"let x = "he«llo woˇ»rld";"#}, cx);
32243 });
32244 editor.update_in(cx, |editor, window, cx| {
32245 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32246 });
32247 editor.update(cx, |editor, cx| {
32248 assert_text_with_selections(editor, indoc! {r#"let x = "he«llo worldˇ»";"#}, cx);
32249 });
32250
32251 // Test Group 4.1: Multiple Cursors - All Expand to Different Syntax Nodes
32252 let text = r#"let x = "hello"; let y = 42;"#.unindent();
32253
32254 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32255 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32256 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32257
32258 editor
32259 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32260 .await;
32261
32262 editor.update_in(cx, |editor, window, cx| {
32263 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32264 s.select_display_ranges([
32265 // Cursor inside string content
32266 DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12),
32267 // Cursor at let statement semicolon
32268 DisplayPoint::new(DisplayRow(0), 18)..DisplayPoint::new(DisplayRow(0), 18),
32269 // Cursor inside integer literal
32270 DisplayPoint::new(DisplayRow(0), 26)..DisplayPoint::new(DisplayRow(0), 26),
32271 ]);
32272 });
32273 });
32274 editor.update(cx, |editor, cx| {
32275 assert_text_with_selections(editor, indoc! {r#"let x = "helˇlo"; lˇet y = 4ˇ2;"#}, cx);
32276 });
32277 editor.update_in(cx, |editor, window, cx| {
32278 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32279 });
32280 editor.update(cx, |editor, cx| {
32281 assert_text_with_selections(editor, indoc! {r#"let x = "hel«loˇ»"; l«et y = 42;ˇ»"#}, cx);
32282 });
32283
32284 // Test Group 4.2: Multiple Cursors on Separate Lines
32285 let text = r#"
32286let x = "hello";
32287let y = 42;
32288"#
32289 .unindent();
32290
32291 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32292 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32293 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32294
32295 editor
32296 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32297 .await;
32298
32299 editor.update_in(cx, |editor, window, cx| {
32300 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32301 s.select_display_ranges([
32302 DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12),
32303 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9),
32304 ]);
32305 });
32306 });
32307
32308 editor.update(cx, |editor, cx| {
32309 assert_text_with_selections(
32310 editor,
32311 indoc! {r#"
32312 let x = "helˇlo";
32313 let y = 4ˇ2;
32314 "#},
32315 cx,
32316 );
32317 });
32318 editor.update_in(cx, |editor, window, cx| {
32319 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32320 });
32321 editor.update(cx, |editor, cx| {
32322 assert_text_with_selections(
32323 editor,
32324 indoc! {r#"
32325 let x = "hel«loˇ»";
32326 let y = 4«2ˇ»;
32327 "#},
32328 cx,
32329 );
32330 });
32331
32332 // Test Group 5.1: Nested Function Calls
32333 let text = r#"let result = foo(bar("arg"));"#.unindent();
32334
32335 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32336 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32337 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32338
32339 editor
32340 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32341 .await;
32342
32343 editor.update_in(cx, |editor, window, cx| {
32344 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32345 s.select_display_ranges([
32346 DisplayPoint::new(DisplayRow(0), 22)..DisplayPoint::new(DisplayRow(0), 22)
32347 ]);
32348 });
32349 });
32350 editor.update(cx, |editor, cx| {
32351 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("ˇarg"));"#}, cx);
32352 });
32353 editor.update_in(cx, |editor, window, cx| {
32354 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32355 });
32356 editor.update(cx, |editor, cx| {
32357 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«argˇ»"));"#}, cx);
32358 });
32359 editor.update_in(cx, |editor, window, cx| {
32360 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32361 });
32362 editor.update(cx, |editor, cx| {
32363 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«arg"ˇ»));"#}, cx);
32364 });
32365 editor.update_in(cx, |editor, window, cx| {
32366 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32367 });
32368 editor.update(cx, |editor, cx| {
32369 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«arg")ˇ»);"#}, cx);
32370 });
32371
32372 // Test Group 6.1: Block Comments
32373 let text = r#"let x = /* multi
32374 line
32375 comment */;"#
32376 .unindent();
32377
32378 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32379 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32380 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32381
32382 editor
32383 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32384 .await;
32385
32386 editor.update_in(cx, |editor, window, cx| {
32387 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32388 s.select_display_ranges([
32389 DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16)
32390 ]);
32391 });
32392 });
32393 editor.update(cx, |editor, cx| {
32394 assert_text_with_selections(
32395 editor,
32396 indoc! {r#"
32397let x = /* multiˇ
32398line
32399comment */;"#},
32400 cx,
32401 );
32402 });
32403 editor.update_in(cx, |editor, window, cx| {
32404 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32405 });
32406 editor.update(cx, |editor, cx| {
32407 assert_text_with_selections(
32408 editor,
32409 indoc! {r#"
32410let x = /* multi«
32411line
32412comment */ˇ»;"#},
32413 cx,
32414 );
32415 });
32416
32417 // Test Group 6.2: Array/Vector Literals
32418 let text = r#"let arr = [1, 2, 3];"#.unindent();
32419
32420 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32421 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32422 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32423
32424 editor
32425 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32426 .await;
32427
32428 editor.update_in(cx, |editor, window, cx| {
32429 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32430 s.select_display_ranges([
32431 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
32432 ]);
32433 });
32434 });
32435 editor.update(cx, |editor, cx| {
32436 assert_text_with_selections(editor, indoc! {r#"let arr = [ˇ1, 2, 3];"#}, cx);
32437 });
32438 editor.update_in(cx, |editor, window, cx| {
32439 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32440 });
32441 editor.update(cx, |editor, cx| {
32442 assert_text_with_selections(editor, indoc! {r#"let arr = [«1ˇ», 2, 3];"#}, cx);
32443 });
32444 editor.update_in(cx, |editor, window, cx| {
32445 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32446 });
32447 editor.update(cx, |editor, cx| {
32448 assert_text_with_selections(editor, indoc! {r#"let arr = [«1, 2, 3]ˇ»;"#}, cx);
32449 });
32450}