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 .collect_vec();
29131 let actual_relative_numbers = snapshot
29132 .calculate_relative_line_numbers(
29133 &(DisplayRow(0)..DisplayRow(24)),
29134 base_display_row,
29135 false,
29136 )
29137 .into_iter()
29138 .sorted()
29139 .collect_vec();
29140 assert_eq!(expected_relative_numbers, actual_relative_numbers);
29141 // check `calculate_relative_line_numbers()` against `relative_line_delta()` for each line
29142 for (display_row, relative_number) in expected_relative_numbers {
29143 assert_eq!(
29144 relative_number,
29145 snapshot
29146 .relative_line_delta(display_row, base_display_row, false)
29147 .unsigned_abs() as u32,
29148 );
29149 }
29150
29151 // test counting wrapped lines
29152 let expected_wrapped_relative_numbers = expected_wrapped_line_numbers
29153 .into_iter()
29154 .enumerate()
29155 .map(|(i, row)| (DisplayRow(row), i.abs_diff(wrapped_base_row) as u32))
29156 .filter(|(row, _)| *row != base_display_row)
29157 .collect_vec();
29158 let actual_relative_numbers = snapshot
29159 .calculate_relative_line_numbers(
29160 &(DisplayRow(0)..DisplayRow(24)),
29161 base_display_row,
29162 true,
29163 )
29164 .into_iter()
29165 .sorted()
29166 .collect_vec();
29167 assert_eq!(expected_wrapped_relative_numbers, actual_relative_numbers);
29168 // check `calculate_relative_line_numbers()` against `relative_wrapped_line_delta()` for each line
29169 for (display_row, relative_number) in expected_wrapped_relative_numbers {
29170 assert_eq!(
29171 relative_number,
29172 snapshot
29173 .relative_line_delta(display_row, base_display_row, true)
29174 .unsigned_abs() as u32,
29175 );
29176 }
29177 });
29178}
29179
29180#[gpui::test]
29181async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
29182 init_test(cx, |_| {});
29183 cx.update(|cx| {
29184 SettingsStore::update_global(cx, |store, cx| {
29185 store.update_user_settings(cx, |settings| {
29186 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
29187 enabled: Some(true),
29188 })
29189 });
29190 });
29191 });
29192 let mut cx = EditorTestContext::new(cx).await;
29193
29194 let line_height = cx.update_editor(|editor, window, cx| {
29195 editor
29196 .style(cx)
29197 .text
29198 .line_height_in_pixels(window.rem_size())
29199 });
29200
29201 let buffer = indoc! {"
29202 ˇfn foo() {
29203 let abc = 123;
29204 }
29205 struct Bar;
29206 impl Bar {
29207 fn new() -> Self {
29208 Self
29209 }
29210 }
29211 fn baz() {
29212 }
29213 "};
29214 cx.set_state(&buffer);
29215
29216 cx.update_editor(|e, _, cx| {
29217 e.buffer()
29218 .read(cx)
29219 .as_singleton()
29220 .unwrap()
29221 .update(cx, |buffer, cx| {
29222 buffer.set_language(Some(rust_lang()), cx);
29223 })
29224 });
29225
29226 let fn_foo = || empty_range(0, 0);
29227 let impl_bar = || empty_range(4, 0);
29228 let fn_new = || empty_range(5, 4);
29229
29230 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
29231 cx.update_editor(|e, window, cx| {
29232 e.scroll(
29233 gpui::Point {
29234 x: 0.,
29235 y: scroll_offset,
29236 },
29237 None,
29238 window,
29239 cx,
29240 );
29241 });
29242 cx.run_until_parked();
29243 cx.simulate_click(
29244 gpui::Point {
29245 x: px(0.),
29246 y: click_offset as f32 * line_height,
29247 },
29248 Modifiers::none(),
29249 );
29250 cx.run_until_parked();
29251 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
29252 };
29253 assert_eq!(
29254 scroll_and_click(
29255 4.5, // impl Bar is halfway off the screen
29256 0.0 // click top of screen
29257 ),
29258 // scrolled to impl Bar
29259 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
29260 );
29261
29262 assert_eq!(
29263 scroll_and_click(
29264 4.5, // impl Bar is halfway off the screen
29265 0.25 // click middle of impl Bar
29266 ),
29267 // scrolled to impl Bar
29268 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
29269 );
29270
29271 assert_eq!(
29272 scroll_and_click(
29273 4.5, // impl Bar is halfway off the screen
29274 1.5 // click below impl Bar (e.g. fn new())
29275 ),
29276 // scrolled to fn new() - this is below the impl Bar header which has persisted
29277 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
29278 );
29279
29280 assert_eq!(
29281 scroll_and_click(
29282 5.5, // fn new is halfway underneath impl Bar
29283 0.75 // click on the overlap of impl Bar and fn new()
29284 ),
29285 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
29286 );
29287
29288 assert_eq!(
29289 scroll_and_click(
29290 5.5, // fn new is halfway underneath impl Bar
29291 1.25 // click on the visible part of fn new()
29292 ),
29293 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
29294 );
29295
29296 assert_eq!(
29297 scroll_and_click(
29298 1.5, // fn foo is halfway off the screen
29299 0.0 // click top of screen
29300 ),
29301 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
29302 );
29303
29304 assert_eq!(
29305 scroll_and_click(
29306 1.5, // fn foo is halfway off the screen
29307 0.75 // click visible part of let abc...
29308 )
29309 .0,
29310 // no change in scroll
29311 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
29312 (gpui::Point { x: 0., y: 1.5 })
29313 );
29314}
29315
29316#[gpui::test]
29317async fn test_next_prev_reference(cx: &mut TestAppContext) {
29318 const CYCLE_POSITIONS: &[&'static str] = &[
29319 indoc! {"
29320 fn foo() {
29321 let ˇabc = 123;
29322 let x = abc + 1;
29323 let y = abc + 2;
29324 let z = abc + 2;
29325 }
29326 "},
29327 indoc! {"
29328 fn foo() {
29329 let abc = 123;
29330 let x = ˇabc + 1;
29331 let y = abc + 2;
29332 let z = abc + 2;
29333 }
29334 "},
29335 indoc! {"
29336 fn foo() {
29337 let abc = 123;
29338 let x = abc + 1;
29339 let y = ˇabc + 2;
29340 let z = abc + 2;
29341 }
29342 "},
29343 indoc! {"
29344 fn foo() {
29345 let abc = 123;
29346 let x = abc + 1;
29347 let y = abc + 2;
29348 let z = ˇabc + 2;
29349 }
29350 "},
29351 ];
29352
29353 init_test(cx, |_| {});
29354
29355 let mut cx = EditorLspTestContext::new_rust(
29356 lsp::ServerCapabilities {
29357 references_provider: Some(lsp::OneOf::Left(true)),
29358 ..Default::default()
29359 },
29360 cx,
29361 )
29362 .await;
29363
29364 // importantly, the cursor is in the middle
29365 cx.set_state(indoc! {"
29366 fn foo() {
29367 let aˇbc = 123;
29368 let x = abc + 1;
29369 let y = abc + 2;
29370 let z = abc + 2;
29371 }
29372 "});
29373
29374 let reference_ranges = [
29375 lsp::Position::new(1, 8),
29376 lsp::Position::new(2, 12),
29377 lsp::Position::new(3, 12),
29378 lsp::Position::new(4, 12),
29379 ]
29380 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
29381
29382 cx.lsp
29383 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
29384 Ok(Some(
29385 reference_ranges
29386 .map(|range| lsp::Location {
29387 uri: params.text_document_position.text_document.uri.clone(),
29388 range,
29389 })
29390 .to_vec(),
29391 ))
29392 });
29393
29394 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
29395 cx.update_editor(|editor, window, cx| {
29396 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
29397 })
29398 .unwrap()
29399 .await
29400 .unwrap()
29401 };
29402
29403 _move(Direction::Next, 1, &mut cx).await;
29404 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29405
29406 _move(Direction::Next, 1, &mut cx).await;
29407 cx.assert_editor_state(CYCLE_POSITIONS[2]);
29408
29409 _move(Direction::Next, 1, &mut cx).await;
29410 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29411
29412 // loops back to the start
29413 _move(Direction::Next, 1, &mut cx).await;
29414 cx.assert_editor_state(CYCLE_POSITIONS[0]);
29415
29416 // loops back to the end
29417 _move(Direction::Prev, 1, &mut cx).await;
29418 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29419
29420 _move(Direction::Prev, 1, &mut cx).await;
29421 cx.assert_editor_state(CYCLE_POSITIONS[2]);
29422
29423 _move(Direction::Prev, 1, &mut cx).await;
29424 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29425
29426 _move(Direction::Prev, 1, &mut cx).await;
29427 cx.assert_editor_state(CYCLE_POSITIONS[0]);
29428
29429 _move(Direction::Next, 3, &mut cx).await;
29430 cx.assert_editor_state(CYCLE_POSITIONS[3]);
29431
29432 _move(Direction::Prev, 2, &mut cx).await;
29433 cx.assert_editor_state(CYCLE_POSITIONS[1]);
29434}
29435
29436#[gpui::test]
29437async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
29438 init_test(cx, |_| {});
29439
29440 let (editor, cx) = cx.add_window_view(|window, cx| {
29441 let multi_buffer = MultiBuffer::build_multi(
29442 [
29443 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29444 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29445 ],
29446 cx,
29447 );
29448 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
29449 });
29450
29451 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29452 let buffer_ids = cx.multibuffer(|mb, _| mb.excerpt_buffer_ids());
29453
29454 cx.assert_excerpts_with_selections(indoc! {"
29455 [EXCERPT]
29456 ˇ1
29457 2
29458 3
29459 [EXCERPT]
29460 1
29461 2
29462 3
29463 "});
29464
29465 // Scenario 1: Unfolded buffers, position cursor on "2", select all matches, then insert
29466 cx.update_editor(|editor, window, cx| {
29467 editor.change_selections(None.into(), window, cx, |s| {
29468 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29469 });
29470 });
29471 cx.assert_excerpts_with_selections(indoc! {"
29472 [EXCERPT]
29473 1
29474 2ˇ
29475 3
29476 [EXCERPT]
29477 1
29478 2
29479 3
29480 "});
29481
29482 cx.update_editor(|editor, window, cx| {
29483 editor
29484 .select_all_matches(&SelectAllMatches, window, cx)
29485 .unwrap();
29486 });
29487 cx.assert_excerpts_with_selections(indoc! {"
29488 [EXCERPT]
29489 1
29490 2ˇ
29491 3
29492 [EXCERPT]
29493 1
29494 2ˇ
29495 3
29496 "});
29497
29498 cx.update_editor(|editor, window, cx| {
29499 editor.handle_input("X", window, cx);
29500 });
29501 cx.assert_excerpts_with_selections(indoc! {"
29502 [EXCERPT]
29503 1
29504 Xˇ
29505 3
29506 [EXCERPT]
29507 1
29508 Xˇ
29509 3
29510 "});
29511
29512 // Scenario 2: Select "2", then fold second buffer before insertion
29513 cx.update_multibuffer(|mb, cx| {
29514 for buffer_id in buffer_ids.iter() {
29515 let buffer = mb.buffer(*buffer_id).unwrap();
29516 buffer.update(cx, |buffer, cx| {
29517 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
29518 });
29519 }
29520 });
29521
29522 // Select "2" and select all matches
29523 cx.update_editor(|editor, window, cx| {
29524 editor.change_selections(None.into(), window, cx, |s| {
29525 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29526 });
29527 editor
29528 .select_all_matches(&SelectAllMatches, window, cx)
29529 .unwrap();
29530 });
29531
29532 // Fold second buffer - should remove selections from folded buffer
29533 cx.update_editor(|editor, _, cx| {
29534 editor.fold_buffer(buffer_ids[1], cx);
29535 });
29536 cx.assert_excerpts_with_selections(indoc! {"
29537 [EXCERPT]
29538 1
29539 2ˇ
29540 3
29541 [EXCERPT]
29542 [FOLDED]
29543 "});
29544
29545 // Insert text - should only affect first buffer
29546 cx.update_editor(|editor, window, cx| {
29547 editor.handle_input("Y", window, cx);
29548 });
29549 cx.update_editor(|editor, _, cx| {
29550 editor.unfold_buffer(buffer_ids[1], cx);
29551 });
29552 cx.assert_excerpts_with_selections(indoc! {"
29553 [EXCERPT]
29554 1
29555 Yˇ
29556 3
29557 [EXCERPT]
29558 1
29559 2
29560 3
29561 "});
29562
29563 // Scenario 3: Select "2", then fold first buffer before insertion
29564 cx.update_multibuffer(|mb, cx| {
29565 for buffer_id in buffer_ids.iter() {
29566 let buffer = mb.buffer(*buffer_id).unwrap();
29567 buffer.update(cx, |buffer, cx| {
29568 buffer.edit([(0..buffer.len(), "1\n2\n3\n")], None, cx);
29569 });
29570 }
29571 });
29572
29573 // Select "2" and select all matches
29574 cx.update_editor(|editor, window, cx| {
29575 editor.change_selections(None.into(), window, cx, |s| {
29576 s.select_ranges([MultiBufferOffset(2)..MultiBufferOffset(3)]);
29577 });
29578 editor
29579 .select_all_matches(&SelectAllMatches, window, cx)
29580 .unwrap();
29581 });
29582
29583 // Fold first buffer - should remove selections from folded buffer
29584 cx.update_editor(|editor, _, cx| {
29585 editor.fold_buffer(buffer_ids[0], cx);
29586 });
29587 cx.assert_excerpts_with_selections(indoc! {"
29588 [EXCERPT]
29589 [FOLDED]
29590 [EXCERPT]
29591 1
29592 2ˇ
29593 3
29594 "});
29595
29596 // Insert text - should only affect second buffer
29597 cx.update_editor(|editor, window, cx| {
29598 editor.handle_input("Z", window, cx);
29599 });
29600 cx.update_editor(|editor, _, cx| {
29601 editor.unfold_buffer(buffer_ids[0], cx);
29602 });
29603 cx.assert_excerpts_with_selections(indoc! {"
29604 [EXCERPT]
29605 1
29606 2
29607 3
29608 [EXCERPT]
29609 1
29610 Zˇ
29611 3
29612 "});
29613
29614 // Test correct folded header is selected upon fold
29615 cx.update_editor(|editor, _, cx| {
29616 editor.fold_buffer(buffer_ids[0], cx);
29617 editor.fold_buffer(buffer_ids[1], cx);
29618 });
29619 cx.assert_excerpts_with_selections(indoc! {"
29620 [EXCERPT]
29621 [FOLDED]
29622 [EXCERPT]
29623 ˇ[FOLDED]
29624 "});
29625
29626 // Test selection inside folded buffer unfolds it on type
29627 cx.update_editor(|editor, window, cx| {
29628 editor.handle_input("W", window, cx);
29629 });
29630 cx.update_editor(|editor, _, cx| {
29631 editor.unfold_buffer(buffer_ids[0], cx);
29632 });
29633 cx.assert_excerpts_with_selections(indoc! {"
29634 [EXCERPT]
29635 1
29636 2
29637 3
29638 [EXCERPT]
29639 Wˇ1
29640 Z
29641 3
29642 "});
29643}
29644
29645#[gpui::test]
29646async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
29647 init_test(cx, |_| {});
29648
29649 let (editor, cx) = cx.add_window_view(|window, cx| {
29650 let multi_buffer = MultiBuffer::build_multi(
29651 [
29652 ("1\n2\n3\n", vec![Point::row_range(0..3)]),
29653 ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
29654 ],
29655 cx,
29656 );
29657 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
29658 });
29659
29660 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
29661
29662 cx.assert_excerpts_with_selections(indoc! {"
29663 [EXCERPT]
29664 ˇ1
29665 2
29666 3
29667 [EXCERPT]
29668 1
29669 2
29670 3
29671 4
29672 5
29673 6
29674 7
29675 8
29676 9
29677 "});
29678
29679 cx.update_editor(|editor, window, cx| {
29680 editor.change_selections(None.into(), window, cx, |s| {
29681 s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
29682 });
29683 });
29684
29685 cx.assert_excerpts_with_selections(indoc! {"
29686 [EXCERPT]
29687 1
29688 2
29689 3
29690 [EXCERPT]
29691 1
29692 2
29693 3
29694 4
29695 5
29696 6
29697 ˇ7
29698 8
29699 9
29700 "});
29701
29702 cx.update_editor(|editor, _window, cx| {
29703 editor.set_vertical_scroll_margin(0, cx);
29704 });
29705
29706 cx.update_editor(|editor, window, cx| {
29707 assert_eq!(editor.vertical_scroll_margin(), 0);
29708 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29709 assert_eq!(
29710 editor.snapshot(window, cx).scroll_position(),
29711 gpui::Point::new(0., 12.0)
29712 );
29713 });
29714
29715 cx.update_editor(|editor, _window, cx| {
29716 editor.set_vertical_scroll_margin(3, cx);
29717 });
29718
29719 cx.update_editor(|editor, window, cx| {
29720 assert_eq!(editor.vertical_scroll_margin(), 3);
29721 editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
29722 assert_eq!(
29723 editor.snapshot(window, cx).scroll_position(),
29724 gpui::Point::new(0., 9.0)
29725 );
29726 });
29727}
29728
29729#[gpui::test]
29730async fn test_find_references_single_case(cx: &mut TestAppContext) {
29731 init_test(cx, |_| {});
29732 let mut cx = EditorLspTestContext::new_rust(
29733 lsp::ServerCapabilities {
29734 references_provider: Some(lsp::OneOf::Left(true)),
29735 ..lsp::ServerCapabilities::default()
29736 },
29737 cx,
29738 )
29739 .await;
29740
29741 let before = indoc!(
29742 r#"
29743 fn main() {
29744 let aˇbc = 123;
29745 let xyz = abc;
29746 }
29747 "#
29748 );
29749 let after = indoc!(
29750 r#"
29751 fn main() {
29752 let abc = 123;
29753 let xyz = ˇabc;
29754 }
29755 "#
29756 );
29757
29758 cx.lsp
29759 .set_request_handler::<lsp::request::References, _, _>(async move |params, _| {
29760 Ok(Some(vec![
29761 lsp::Location {
29762 uri: params.text_document_position.text_document.uri.clone(),
29763 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 11)),
29764 },
29765 lsp::Location {
29766 uri: params.text_document_position.text_document.uri,
29767 range: lsp::Range::new(lsp::Position::new(2, 14), lsp::Position::new(2, 17)),
29768 },
29769 ]))
29770 });
29771
29772 cx.set_state(before);
29773
29774 let action = FindAllReferences {
29775 always_open_multibuffer: false,
29776 };
29777
29778 let navigated = cx
29779 .update_editor(|editor, window, cx| editor.find_all_references(&action, window, cx))
29780 .expect("should have spawned a task")
29781 .await
29782 .unwrap();
29783
29784 assert_eq!(navigated, Navigated::No);
29785
29786 cx.run_until_parked();
29787
29788 cx.assert_editor_state(after);
29789}
29790
29791#[gpui::test]
29792async fn test_newline_task_list_continuation(cx: &mut TestAppContext) {
29793 init_test(cx, |settings| {
29794 settings.defaults.tab_size = Some(2.try_into().unwrap());
29795 });
29796
29797 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29798 let mut cx = EditorTestContext::new(cx).await;
29799 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29800
29801 // Case 1: Adding newline after (whitespace + prefix + any non-whitespace) adds marker
29802 cx.set_state(indoc! {"
29803 - [ ] taskˇ
29804 "});
29805 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29806 cx.wait_for_autoindent_applied().await;
29807 cx.assert_editor_state(indoc! {"
29808 - [ ] task
29809 - [ ] ˇ
29810 "});
29811
29812 // Case 2: Works with checked task items too
29813 cx.set_state(indoc! {"
29814 - [x] completed taskˇ
29815 "});
29816 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29817 cx.wait_for_autoindent_applied().await;
29818 cx.assert_editor_state(indoc! {"
29819 - [x] completed task
29820 - [ ] ˇ
29821 "});
29822
29823 // Case 2.1: Works with uppercase checked marker too
29824 cx.set_state(indoc! {"
29825 - [X] completed taskˇ
29826 "});
29827 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29828 cx.wait_for_autoindent_applied().await;
29829 cx.assert_editor_state(indoc! {"
29830 - [X] completed task
29831 - [ ] ˇ
29832 "});
29833
29834 // Case 3: Cursor position doesn't matter - content after marker is what counts
29835 cx.set_state(indoc! {"
29836 - [ ] taˇsk
29837 "});
29838 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29839 cx.wait_for_autoindent_applied().await;
29840 cx.assert_editor_state(indoc! {"
29841 - [ ] ta
29842 - [ ] ˇsk
29843 "});
29844
29845 // Case 4: Adding newline after (whitespace + prefix + some whitespace) does NOT add marker
29846 cx.set_state(indoc! {"
29847 - [ ] ˇ
29848 "});
29849 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29850 cx.wait_for_autoindent_applied().await;
29851 cx.assert_editor_state(
29852 indoc! {"
29853 - [ ]$$
29854 ˇ
29855 "}
29856 .replace("$", " ")
29857 .as_str(),
29858 );
29859
29860 // Case 5: Adding newline with content adds marker preserving indentation
29861 cx.set_state(indoc! {"
29862 - [ ] task
29863 - [ ] indentedˇ
29864 "});
29865 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29866 cx.wait_for_autoindent_applied().await;
29867 cx.assert_editor_state(indoc! {"
29868 - [ ] task
29869 - [ ] indented
29870 - [ ] ˇ
29871 "});
29872
29873 // Case 6: Adding newline with cursor right after prefix, unindents
29874 cx.set_state(indoc! {"
29875 - [ ] task
29876 - [ ] sub task
29877 - [ ] ˇ
29878 "});
29879 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29880 cx.wait_for_autoindent_applied().await;
29881 cx.assert_editor_state(indoc! {"
29882 - [ ] task
29883 - [ ] sub task
29884 - [ ] ˇ
29885 "});
29886 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29887 cx.wait_for_autoindent_applied().await;
29888
29889 // Case 7: Adding newline with cursor right after prefix, removes marker
29890 cx.assert_editor_state(indoc! {"
29891 - [ ] task
29892 - [ ] sub task
29893 - [ ] ˇ
29894 "});
29895 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29896 cx.wait_for_autoindent_applied().await;
29897 cx.assert_editor_state(indoc! {"
29898 - [ ] task
29899 - [ ] sub task
29900 ˇ
29901 "});
29902
29903 // Case 8: Cursor before or inside prefix does not add marker
29904 cx.set_state(indoc! {"
29905 ˇ- [ ] task
29906 "});
29907 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29908 cx.wait_for_autoindent_applied().await;
29909 cx.assert_editor_state(indoc! {"
29910
29911 ˇ- [ ] task
29912 "});
29913
29914 cx.set_state(indoc! {"
29915 - [ˇ ] task
29916 "});
29917 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29918 cx.wait_for_autoindent_applied().await;
29919 cx.assert_editor_state(indoc! {"
29920 - [
29921 ˇ
29922 ] task
29923 "});
29924}
29925
29926#[gpui::test]
29927async fn test_newline_unordered_list_continuation(cx: &mut TestAppContext) {
29928 init_test(cx, |settings| {
29929 settings.defaults.tab_size = Some(2.try_into().unwrap());
29930 });
29931
29932 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
29933 let mut cx = EditorTestContext::new(cx).await;
29934 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
29935
29936 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) adds marker
29937 cx.set_state(indoc! {"
29938 - itemˇ
29939 "});
29940 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29941 cx.wait_for_autoindent_applied().await;
29942 cx.assert_editor_state(indoc! {"
29943 - item
29944 - ˇ
29945 "});
29946
29947 // Case 2: Works with different markers
29948 cx.set_state(indoc! {"
29949 * starred itemˇ
29950 "});
29951 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29952 cx.wait_for_autoindent_applied().await;
29953 cx.assert_editor_state(indoc! {"
29954 * starred item
29955 * ˇ
29956 "});
29957
29958 cx.set_state(indoc! {"
29959 + plus itemˇ
29960 "});
29961 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29962 cx.wait_for_autoindent_applied().await;
29963 cx.assert_editor_state(indoc! {"
29964 + plus item
29965 + ˇ
29966 "});
29967
29968 // Case 3: Cursor position doesn't matter - content after marker is what counts
29969 cx.set_state(indoc! {"
29970 - itˇem
29971 "});
29972 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29973 cx.wait_for_autoindent_applied().await;
29974 cx.assert_editor_state(indoc! {"
29975 - it
29976 - ˇem
29977 "});
29978
29979 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
29980 cx.set_state(indoc! {"
29981 - ˇ
29982 "});
29983 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
29984 cx.wait_for_autoindent_applied().await;
29985 cx.assert_editor_state(
29986 indoc! {"
29987 - $
29988 ˇ
29989 "}
29990 .replace("$", " ")
29991 .as_str(),
29992 );
29993
29994 // Case 5: Adding newline with content adds marker preserving indentation
29995 cx.set_state(indoc! {"
29996 - item
29997 - indentedˇ
29998 "});
29999 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30000 cx.wait_for_autoindent_applied().await;
30001 cx.assert_editor_state(indoc! {"
30002 - item
30003 - indented
30004 - ˇ
30005 "});
30006
30007 // Case 6: Adding newline with cursor right after marker, unindents
30008 cx.set_state(indoc! {"
30009 - item
30010 - sub item
30011 - ˇ
30012 "});
30013 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30014 cx.wait_for_autoindent_applied().await;
30015 cx.assert_editor_state(indoc! {"
30016 - item
30017 - sub item
30018 - ˇ
30019 "});
30020 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30021 cx.wait_for_autoindent_applied().await;
30022
30023 // Case 7: Adding newline with cursor right after marker, removes marker
30024 cx.assert_editor_state(indoc! {"
30025 - item
30026 - sub item
30027 - ˇ
30028 "});
30029 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30030 cx.wait_for_autoindent_applied().await;
30031 cx.assert_editor_state(indoc! {"
30032 - item
30033 - sub item
30034 ˇ
30035 "});
30036
30037 // Case 8: Cursor before or inside prefix does not add marker
30038 cx.set_state(indoc! {"
30039 ˇ- item
30040 "});
30041 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30042 cx.wait_for_autoindent_applied().await;
30043 cx.assert_editor_state(indoc! {"
30044
30045 ˇ- item
30046 "});
30047
30048 cx.set_state(indoc! {"
30049 -ˇ item
30050 "});
30051 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30052 cx.wait_for_autoindent_applied().await;
30053 cx.assert_editor_state(indoc! {"
30054 -
30055 ˇitem
30056 "});
30057}
30058
30059#[gpui::test]
30060async fn test_newline_ordered_list_continuation(cx: &mut TestAppContext) {
30061 init_test(cx, |settings| {
30062 settings.defaults.tab_size = Some(2.try_into().unwrap());
30063 });
30064
30065 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
30066 let mut cx = EditorTestContext::new(cx).await;
30067 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
30068
30069 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
30070 cx.set_state(indoc! {"
30071 1. first itemˇ
30072 "});
30073 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30074 cx.wait_for_autoindent_applied().await;
30075 cx.assert_editor_state(indoc! {"
30076 1. first item
30077 2. ˇ
30078 "});
30079
30080 // Case 2: Works with larger numbers
30081 cx.set_state(indoc! {"
30082 10. tenth itemˇ
30083 "});
30084 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30085 cx.wait_for_autoindent_applied().await;
30086 cx.assert_editor_state(indoc! {"
30087 10. tenth item
30088 11. ˇ
30089 "});
30090
30091 // Case 3: Cursor position doesn't matter - content after marker is what counts
30092 cx.set_state(indoc! {"
30093 1. itˇem
30094 "});
30095 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30096 cx.wait_for_autoindent_applied().await;
30097 cx.assert_editor_state(indoc! {"
30098 1. it
30099 2. ˇem
30100 "});
30101
30102 // Case 4: Adding newline after (whitespace + marker + some whitespace) does NOT add marker
30103 cx.set_state(indoc! {"
30104 1. ˇ
30105 "});
30106 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30107 cx.wait_for_autoindent_applied().await;
30108 cx.assert_editor_state(
30109 indoc! {"
30110 1. $
30111 ˇ
30112 "}
30113 .replace("$", " ")
30114 .as_str(),
30115 );
30116
30117 // Case 5: Adding newline with content adds marker preserving indentation
30118 cx.set_state(indoc! {"
30119 1. item
30120 2. indentedˇ
30121 "});
30122 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30123 cx.wait_for_autoindent_applied().await;
30124 cx.assert_editor_state(indoc! {"
30125 1. item
30126 2. indented
30127 3. ˇ
30128 "});
30129
30130 // Case 6: Adding newline with cursor right after marker, unindents
30131 cx.set_state(indoc! {"
30132 1. item
30133 2. sub item
30134 3. ˇ
30135 "});
30136 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30137 cx.wait_for_autoindent_applied().await;
30138 cx.assert_editor_state(indoc! {"
30139 1. item
30140 2. sub item
30141 1. ˇ
30142 "});
30143 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30144 cx.wait_for_autoindent_applied().await;
30145
30146 // Case 7: Adding newline with cursor right after marker, removes marker
30147 cx.assert_editor_state(indoc! {"
30148 1. item
30149 2. sub item
30150 1. ˇ
30151 "});
30152 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30153 cx.wait_for_autoindent_applied().await;
30154 cx.assert_editor_state(indoc! {"
30155 1. item
30156 2. sub item
30157 ˇ
30158 "});
30159
30160 // Case 8: Cursor before or inside prefix does not add marker
30161 cx.set_state(indoc! {"
30162 ˇ1. item
30163 "});
30164 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30165 cx.wait_for_autoindent_applied().await;
30166 cx.assert_editor_state(indoc! {"
30167
30168 ˇ1. item
30169 "});
30170
30171 cx.set_state(indoc! {"
30172 1ˇ. item
30173 "});
30174 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30175 cx.wait_for_autoindent_applied().await;
30176 cx.assert_editor_state(indoc! {"
30177 1
30178 ˇ. item
30179 "});
30180}
30181
30182#[gpui::test]
30183async fn test_newline_should_not_autoindent_ordered_list(cx: &mut TestAppContext) {
30184 init_test(cx, |settings| {
30185 settings.defaults.tab_size = Some(2.try_into().unwrap());
30186 });
30187
30188 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
30189 let mut cx = EditorTestContext::new(cx).await;
30190 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
30191
30192 // Case 1: Adding newline after (whitespace + marker + any non-whitespace) increments number
30193 cx.set_state(indoc! {"
30194 1. first item
30195 1. sub first item
30196 2. sub second item
30197 3. ˇ
30198 "});
30199 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
30200 cx.wait_for_autoindent_applied().await;
30201 cx.assert_editor_state(indoc! {"
30202 1. first item
30203 1. sub first item
30204 2. sub second item
30205 1. ˇ
30206 "});
30207}
30208
30209#[gpui::test]
30210async fn test_tab_list_indent(cx: &mut TestAppContext) {
30211 init_test(cx, |settings| {
30212 settings.defaults.tab_size = Some(2.try_into().unwrap());
30213 });
30214
30215 let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
30216 let mut cx = EditorTestContext::new(cx).await;
30217 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
30218
30219 // Case 1: Unordered list - cursor after prefix, adds indent before prefix
30220 cx.set_state(indoc! {"
30221 - ˇitem
30222 "});
30223 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30224 cx.wait_for_autoindent_applied().await;
30225 let expected = indoc! {"
30226 $$- ˇitem
30227 "};
30228 cx.assert_editor_state(expected.replace("$", " ").as_str());
30229
30230 // Case 2: Task list - cursor after prefix
30231 cx.set_state(indoc! {"
30232 - [ ] ˇtask
30233 "});
30234 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30235 cx.wait_for_autoindent_applied().await;
30236 let expected = indoc! {"
30237 $$- [ ] ˇtask
30238 "};
30239 cx.assert_editor_state(expected.replace("$", " ").as_str());
30240
30241 // Case 3: Ordered list - cursor after prefix
30242 cx.set_state(indoc! {"
30243 1. ˇfirst
30244 "});
30245 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30246 cx.wait_for_autoindent_applied().await;
30247 let expected = indoc! {"
30248 $$1. ˇfirst
30249 "};
30250 cx.assert_editor_state(expected.replace("$", " ").as_str());
30251
30252 // Case 4: With existing indentation - adds more indent
30253 let initial = indoc! {"
30254 $$- ˇitem
30255 "};
30256 cx.set_state(initial.replace("$", " ").as_str());
30257 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30258 cx.wait_for_autoindent_applied().await;
30259 let expected = indoc! {"
30260 $$$$- ˇitem
30261 "};
30262 cx.assert_editor_state(expected.replace("$", " ").as_str());
30263
30264 // Case 5: Empty list item
30265 cx.set_state(indoc! {"
30266 - ˇ
30267 "});
30268 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30269 cx.wait_for_autoindent_applied().await;
30270 let expected = indoc! {"
30271 $$- ˇ
30272 "};
30273 cx.assert_editor_state(expected.replace("$", " ").as_str());
30274
30275 // Case 6: Cursor at end of line with content
30276 cx.set_state(indoc! {"
30277 - itemˇ
30278 "});
30279 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30280 cx.wait_for_autoindent_applied().await;
30281 let expected = indoc! {"
30282 $$- itemˇ
30283 "};
30284 cx.assert_editor_state(expected.replace("$", " ").as_str());
30285
30286 // Case 7: Cursor at start of list item, indents it
30287 cx.set_state(indoc! {"
30288 - item
30289 ˇ - sub item
30290 "});
30291 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30292 cx.wait_for_autoindent_applied().await;
30293 let expected = indoc! {"
30294 - item
30295 ˇ - sub item
30296 "};
30297 cx.assert_editor_state(expected);
30298
30299 // Case 8: Cursor at start of list item, moves the cursor when "indent_list_on_tab" is false
30300 cx.update_editor(|_, _, cx| {
30301 SettingsStore::update_global(cx, |store, cx| {
30302 store.update_user_settings(cx, |settings| {
30303 settings.project.all_languages.defaults.indent_list_on_tab = Some(false);
30304 });
30305 });
30306 });
30307 cx.set_state(indoc! {"
30308 - item
30309 ˇ - sub item
30310 "});
30311 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
30312 cx.wait_for_autoindent_applied().await;
30313 let expected = indoc! {"
30314 - item
30315 ˇ- sub item
30316 "};
30317 cx.assert_editor_state(expected);
30318}
30319
30320#[gpui::test]
30321async fn test_local_worktree_trust(cx: &mut TestAppContext) {
30322 init_test(cx, |_| {});
30323 cx.update(|cx| project::trusted_worktrees::init(HashMap::default(), cx));
30324
30325 cx.update(|cx| {
30326 SettingsStore::update_global(cx, |store, cx| {
30327 store.update_user_settings(cx, |settings| {
30328 settings.project.all_languages.defaults.inlay_hints =
30329 Some(InlayHintSettingsContent {
30330 enabled: Some(true),
30331 ..InlayHintSettingsContent::default()
30332 });
30333 });
30334 });
30335 });
30336
30337 let fs = FakeFs::new(cx.executor());
30338 fs.insert_tree(
30339 path!("/project"),
30340 json!({
30341 ".zed": {
30342 "settings.json": r#"{"languages":{"Rust":{"language_servers":["override-rust-analyzer"]}}}"#
30343 },
30344 "main.rs": "fn main() {}"
30345 }),
30346 )
30347 .await;
30348
30349 let lsp_inlay_hint_request_count = Arc::new(AtomicUsize::new(0));
30350 let server_name = "override-rust-analyzer";
30351 let project = Project::test_with_worktree_trust(fs, [path!("/project").as_ref()], cx).await;
30352
30353 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
30354 language_registry.add(rust_lang());
30355
30356 let capabilities = lsp::ServerCapabilities {
30357 inlay_hint_provider: Some(lsp::OneOf::Left(true)),
30358 ..lsp::ServerCapabilities::default()
30359 };
30360 let mut fake_language_servers = language_registry.register_fake_lsp(
30361 "Rust",
30362 FakeLspAdapter {
30363 name: server_name,
30364 capabilities,
30365 initializer: Some(Box::new({
30366 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
30367 move |fake_server| {
30368 let lsp_inlay_hint_request_count = lsp_inlay_hint_request_count.clone();
30369 fake_server.set_request_handler::<lsp::request::InlayHintRequest, _, _>(
30370 move |_params, _| {
30371 lsp_inlay_hint_request_count.fetch_add(1, atomic::Ordering::Release);
30372 async move {
30373 Ok(Some(vec![lsp::InlayHint {
30374 position: lsp::Position::new(0, 0),
30375 label: lsp::InlayHintLabel::String("hint".to_string()),
30376 kind: None,
30377 text_edits: None,
30378 tooltip: None,
30379 padding_left: None,
30380 padding_right: None,
30381 data: None,
30382 }]))
30383 }
30384 },
30385 );
30386 }
30387 })),
30388 ..FakeLspAdapter::default()
30389 },
30390 );
30391
30392 cx.run_until_parked();
30393
30394 let worktree_id = project.read_with(cx, |project, cx| {
30395 project
30396 .worktrees(cx)
30397 .next()
30398 .map(|wt| wt.read(cx).id())
30399 .expect("should have a worktree")
30400 });
30401 let worktree_store = project.read_with(cx, |project, _| project.worktree_store());
30402
30403 let trusted_worktrees =
30404 cx.update(|cx| TrustedWorktrees::try_get_global(cx).expect("trust global should exist"));
30405
30406 let can_trust = trusted_worktrees.update(cx, |store, cx| {
30407 store.can_trust(&worktree_store, worktree_id, cx)
30408 });
30409 assert!(!can_trust, "worktree should be restricted initially");
30410
30411 let buffer_before_approval = project
30412 .update(cx, |project, cx| {
30413 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
30414 })
30415 .await
30416 .unwrap();
30417
30418 let (editor, cx) = cx.add_window_view(|window, cx| {
30419 Editor::new(
30420 EditorMode::full(),
30421 cx.new(|cx| MultiBuffer::singleton(buffer_before_approval.clone(), cx)),
30422 Some(project.clone()),
30423 window,
30424 cx,
30425 )
30426 });
30427 cx.run_until_parked();
30428 let fake_language_server = fake_language_servers.next();
30429
30430 cx.read(|cx| {
30431 let file = buffer_before_approval.read(cx).file();
30432 assert_eq!(
30433 language::language_settings::language_settings(Some("Rust".into()), file, cx)
30434 .language_servers,
30435 ["...".to_string()],
30436 "local .zed/settings.json must not apply before trust approval"
30437 )
30438 });
30439
30440 editor.update_in(cx, |editor, window, cx| {
30441 editor.handle_input("1", window, cx);
30442 });
30443 cx.run_until_parked();
30444 cx.executor()
30445 .advance_clock(std::time::Duration::from_secs(1));
30446 assert_eq!(
30447 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire),
30448 0,
30449 "inlay hints must not be queried before trust approval"
30450 );
30451
30452 trusted_worktrees.update(cx, |store, cx| {
30453 store.trust(
30454 &worktree_store,
30455 std::collections::HashSet::from_iter([PathTrust::Worktree(worktree_id)]),
30456 cx,
30457 );
30458 });
30459 cx.run_until_parked();
30460
30461 cx.read(|cx| {
30462 let file = buffer_before_approval.read(cx).file();
30463 assert_eq!(
30464 language::language_settings::language_settings(Some("Rust".into()), file, cx)
30465 .language_servers,
30466 ["override-rust-analyzer".to_string()],
30467 "local .zed/settings.json should apply after trust approval"
30468 )
30469 });
30470 let _fake_language_server = fake_language_server.await.unwrap();
30471 editor.update_in(cx, |editor, window, cx| {
30472 editor.handle_input("1", window, cx);
30473 });
30474 cx.run_until_parked();
30475 cx.executor()
30476 .advance_clock(std::time::Duration::from_secs(1));
30477 assert!(
30478 lsp_inlay_hint_request_count.load(atomic::Ordering::Acquire) > 0,
30479 "inlay hints should be queried after trust approval"
30480 );
30481
30482 let can_trust_after = trusted_worktrees.update(cx, |store, cx| {
30483 store.can_trust(&worktree_store, worktree_id, cx)
30484 });
30485 assert!(can_trust_after, "worktree should be trusted after trust()");
30486}
30487
30488#[gpui::test]
30489fn test_editor_rendering_when_positioned_above_viewport(cx: &mut TestAppContext) {
30490 // This test reproduces a bug where drawing an editor at a position above the viewport
30491 // (simulating what happens when an AutoHeight editor inside a List is scrolled past)
30492 // causes an infinite loop in blocks_in_range.
30493 //
30494 // The issue: when the editor's bounds.origin.y is very negative (above the viewport),
30495 // the content mask intersection produces visible_bounds with origin at the viewport top.
30496 // This makes clipped_top_in_lines very large, causing start_row to exceed max_row.
30497 // When blocks_in_range is called with start_row > max_row, the cursor seeks to the end
30498 // but the while loop after seek never terminates because cursor.next() is a no-op at end.
30499 init_test(cx, |_| {});
30500
30501 let window = cx.add_window(|_, _| gpui::Empty);
30502 let mut cx = VisualTestContext::from_window(*window, cx);
30503
30504 let buffer = cx.update(|_, cx| MultiBuffer::build_simple("a\nb\nc\nd\ne\nf\ng\nh\ni\nj\n", cx));
30505 let editor = cx.new_window_entity(|window, cx| build_editor(buffer, window, cx));
30506
30507 // Simulate a small viewport (500x500 pixels at origin 0,0)
30508 cx.simulate_resize(gpui::size(px(500.), px(500.)));
30509
30510 // Draw the editor at a very negative Y position, simulating an editor that's been
30511 // scrolled way above the visible viewport (like in a List that has scrolled past it).
30512 // The editor is 3000px tall but positioned at y=-10000, so it's entirely above the viewport.
30513 // This should NOT hang - it should just render nothing.
30514 cx.draw(
30515 gpui::point(px(0.), px(-10000.)),
30516 gpui::size(px(500.), px(3000.)),
30517 |_, _| editor.clone().into_any_element(),
30518 );
30519
30520 // If we get here without hanging, the test passes
30521}
30522
30523#[gpui::test]
30524async fn test_diff_review_indicator_created_on_gutter_hover(cx: &mut TestAppContext) {
30525 init_test(cx, |_| {});
30526
30527 let fs = FakeFs::new(cx.executor());
30528 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
30529 .await;
30530
30531 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
30532 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
30533 let cx = &mut VisualTestContext::from_window(*workspace, cx);
30534
30535 let editor = workspace
30536 .update(cx, |workspace, window, cx| {
30537 workspace.open_abs_path(
30538 PathBuf::from(path!("/root/file.txt")),
30539 OpenOptions::default(),
30540 window,
30541 cx,
30542 )
30543 })
30544 .unwrap()
30545 .await
30546 .unwrap()
30547 .downcast::<Editor>()
30548 .unwrap();
30549
30550 // Enable diff review button mode
30551 editor.update(cx, |editor, cx| {
30552 editor.set_show_diff_review_button(true, cx);
30553 });
30554
30555 // Initially, no indicator should be present
30556 editor.update(cx, |editor, _cx| {
30557 assert!(
30558 editor.gutter_diff_review_indicator.0.is_none(),
30559 "Indicator should be None initially"
30560 );
30561 });
30562}
30563
30564#[gpui::test]
30565async fn test_diff_review_button_hidden_when_ai_disabled(cx: &mut TestAppContext) {
30566 init_test(cx, |_| {});
30567
30568 // Register DisableAiSettings and set disable_ai to true
30569 cx.update(|cx| {
30570 project::DisableAiSettings::register(cx);
30571 project::DisableAiSettings::override_global(
30572 project::DisableAiSettings { disable_ai: true },
30573 cx,
30574 );
30575 });
30576
30577 let fs = FakeFs::new(cx.executor());
30578 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
30579 .await;
30580
30581 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
30582 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
30583 let cx = &mut VisualTestContext::from_window(*workspace, cx);
30584
30585 let editor = workspace
30586 .update(cx, |workspace, window, cx| {
30587 workspace.open_abs_path(
30588 PathBuf::from(path!("/root/file.txt")),
30589 OpenOptions::default(),
30590 window,
30591 cx,
30592 )
30593 })
30594 .unwrap()
30595 .await
30596 .unwrap()
30597 .downcast::<Editor>()
30598 .unwrap();
30599
30600 // Enable diff review button mode
30601 editor.update(cx, |editor, cx| {
30602 editor.set_show_diff_review_button(true, cx);
30603 });
30604
30605 // Verify AI is disabled
30606 cx.read(|cx| {
30607 assert!(
30608 project::DisableAiSettings::get_global(cx).disable_ai,
30609 "AI should be disabled"
30610 );
30611 });
30612
30613 // The indicator should not be created when AI is disabled
30614 // (The mouse_moved handler checks DisableAiSettings before creating the indicator)
30615 editor.update(cx, |editor, _cx| {
30616 assert!(
30617 editor.gutter_diff_review_indicator.0.is_none(),
30618 "Indicator should be None when AI is disabled"
30619 );
30620 });
30621}
30622
30623#[gpui::test]
30624async fn test_diff_review_button_shown_when_ai_enabled(cx: &mut TestAppContext) {
30625 init_test(cx, |_| {});
30626
30627 // Register DisableAiSettings and set disable_ai to false
30628 cx.update(|cx| {
30629 project::DisableAiSettings::register(cx);
30630 project::DisableAiSettings::override_global(
30631 project::DisableAiSettings { disable_ai: false },
30632 cx,
30633 );
30634 });
30635
30636 let fs = FakeFs::new(cx.executor());
30637 fs.insert_tree(path!("/root"), json!({ "file.txt": "hello\nworld\n" }))
30638 .await;
30639
30640 let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
30641 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
30642 let cx = &mut VisualTestContext::from_window(*workspace, cx);
30643
30644 let editor = workspace
30645 .update(cx, |workspace, window, cx| {
30646 workspace.open_abs_path(
30647 PathBuf::from(path!("/root/file.txt")),
30648 OpenOptions::default(),
30649 window,
30650 cx,
30651 )
30652 })
30653 .unwrap()
30654 .await
30655 .unwrap()
30656 .downcast::<Editor>()
30657 .unwrap();
30658
30659 // Enable diff review button mode
30660 editor.update(cx, |editor, cx| {
30661 editor.set_show_diff_review_button(true, cx);
30662 });
30663
30664 // Verify AI is enabled
30665 cx.read(|cx| {
30666 assert!(
30667 !project::DisableAiSettings::get_global(cx).disable_ai,
30668 "AI should be enabled"
30669 );
30670 });
30671
30672 // The show_diff_review_button flag should be true
30673 editor.update(cx, |editor, _cx| {
30674 assert!(
30675 editor.show_diff_review_button(),
30676 "show_diff_review_button should be true"
30677 );
30678 });
30679}
30680
30681/// Helper function to create a DiffHunkKey for testing.
30682/// Uses Anchor::min() as a placeholder anchor since these tests don't need
30683/// real buffer positioning.
30684fn test_hunk_key(file_path: &str) -> DiffHunkKey {
30685 DiffHunkKey {
30686 file_path: if file_path.is_empty() {
30687 Arc::from(util::rel_path::RelPath::empty())
30688 } else {
30689 Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
30690 },
30691 hunk_start_anchor: Anchor::min(),
30692 }
30693}
30694
30695/// Helper function to create a DiffHunkKey with a specific anchor for testing.
30696fn test_hunk_key_with_anchor(file_path: &str, anchor: Anchor) -> DiffHunkKey {
30697 DiffHunkKey {
30698 file_path: if file_path.is_empty() {
30699 Arc::from(util::rel_path::RelPath::empty())
30700 } else {
30701 Arc::from(util::rel_path::RelPath::unix(file_path).unwrap())
30702 },
30703 hunk_start_anchor: anchor,
30704 }
30705}
30706
30707/// Helper function to add a review comment with default anchors for testing.
30708fn add_test_comment(
30709 editor: &mut Editor,
30710 key: DiffHunkKey,
30711 comment: &str,
30712 cx: &mut Context<Editor>,
30713) -> usize {
30714 editor.add_review_comment(key, comment.to_string(), Anchor::min()..Anchor::max(), cx)
30715}
30716
30717#[gpui::test]
30718fn test_review_comment_add_to_hunk(cx: &mut TestAppContext) {
30719 init_test(cx, |_| {});
30720
30721 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30722
30723 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30724 let key = test_hunk_key("");
30725
30726 let id = add_test_comment(editor, key.clone(), "Test comment", cx);
30727
30728 let snapshot = editor.buffer().read(cx).snapshot(cx);
30729 assert_eq!(editor.total_review_comment_count(), 1);
30730 assert_eq!(editor.hunk_comment_count(&key, &snapshot), 1);
30731
30732 let comments = editor.comments_for_hunk(&key, &snapshot);
30733 assert_eq!(comments.len(), 1);
30734 assert_eq!(comments[0].comment, "Test comment");
30735 assert_eq!(comments[0].id, id);
30736 });
30737}
30738
30739#[gpui::test]
30740fn test_review_comments_are_per_hunk(cx: &mut TestAppContext) {
30741 init_test(cx, |_| {});
30742
30743 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30744
30745 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30746 let snapshot = editor.buffer().read(cx).snapshot(cx);
30747 let anchor1 = snapshot.anchor_before(Point::new(0, 0));
30748 let anchor2 = snapshot.anchor_before(Point::new(0, 0));
30749 let key1 = test_hunk_key_with_anchor("file1.rs", anchor1);
30750 let key2 = test_hunk_key_with_anchor("file2.rs", anchor2);
30751
30752 add_test_comment(editor, key1.clone(), "Comment for file1", cx);
30753 add_test_comment(editor, key2.clone(), "Comment for file2", cx);
30754
30755 let snapshot = editor.buffer().read(cx).snapshot(cx);
30756 assert_eq!(editor.total_review_comment_count(), 2);
30757 assert_eq!(editor.hunk_comment_count(&key1, &snapshot), 1);
30758 assert_eq!(editor.hunk_comment_count(&key2, &snapshot), 1);
30759
30760 assert_eq!(
30761 editor.comments_for_hunk(&key1, &snapshot)[0].comment,
30762 "Comment for file1"
30763 );
30764 assert_eq!(
30765 editor.comments_for_hunk(&key2, &snapshot)[0].comment,
30766 "Comment for file2"
30767 );
30768 });
30769}
30770
30771#[gpui::test]
30772fn test_review_comment_remove(cx: &mut TestAppContext) {
30773 init_test(cx, |_| {});
30774
30775 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30776
30777 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30778 let key = test_hunk_key("");
30779
30780 let id = add_test_comment(editor, key, "To be removed", cx);
30781
30782 assert_eq!(editor.total_review_comment_count(), 1);
30783
30784 let removed = editor.remove_review_comment(id, cx);
30785 assert!(removed);
30786 assert_eq!(editor.total_review_comment_count(), 0);
30787
30788 // Try to remove again
30789 let removed_again = editor.remove_review_comment(id, cx);
30790 assert!(!removed_again);
30791 });
30792}
30793
30794#[gpui::test]
30795fn test_review_comment_update(cx: &mut TestAppContext) {
30796 init_test(cx, |_| {});
30797
30798 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30799
30800 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30801 let key = test_hunk_key("");
30802
30803 let id = add_test_comment(editor, key.clone(), "Original text", cx);
30804
30805 let updated = editor.update_review_comment(id, "Updated text".to_string(), cx);
30806 assert!(updated);
30807
30808 let snapshot = editor.buffer().read(cx).snapshot(cx);
30809 let comments = editor.comments_for_hunk(&key, &snapshot);
30810 assert_eq!(comments[0].comment, "Updated text");
30811 assert!(!comments[0].is_editing); // Should clear editing flag
30812 });
30813}
30814
30815#[gpui::test]
30816fn test_review_comment_take_all(cx: &mut TestAppContext) {
30817 init_test(cx, |_| {});
30818
30819 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30820
30821 _ = editor.update(cx, |editor: &mut Editor, _window, cx| {
30822 let snapshot = editor.buffer().read(cx).snapshot(cx);
30823 let anchor1 = snapshot.anchor_before(Point::new(0, 0));
30824 let anchor2 = snapshot.anchor_before(Point::new(0, 0));
30825 let key1 = test_hunk_key_with_anchor("file1.rs", anchor1);
30826 let key2 = test_hunk_key_with_anchor("file2.rs", anchor2);
30827
30828 let id1 = add_test_comment(editor, key1.clone(), "Comment 1", cx);
30829 let id2 = add_test_comment(editor, key1.clone(), "Comment 2", cx);
30830 let id3 = add_test_comment(editor, key2.clone(), "Comment 3", cx);
30831
30832 // IDs should be sequential starting from 0
30833 assert_eq!(id1, 0);
30834 assert_eq!(id2, 1);
30835 assert_eq!(id3, 2);
30836
30837 assert_eq!(editor.total_review_comment_count(), 3);
30838
30839 let taken = editor.take_all_review_comments(cx);
30840
30841 // Should have 2 entries (one per hunk)
30842 assert_eq!(taken.len(), 2);
30843
30844 // Total comments should be 3
30845 let total: usize = taken
30846 .iter()
30847 .map(|(_, comments): &(DiffHunkKey, Vec<StoredReviewComment>)| comments.len())
30848 .sum();
30849 assert_eq!(total, 3);
30850
30851 // Storage should be empty
30852 assert_eq!(editor.total_review_comment_count(), 0);
30853
30854 // After taking all comments, ID counter should reset
30855 // New comments should get IDs starting from 0 again
30856 let new_id1 = add_test_comment(editor, key1, "New Comment 1", cx);
30857 let new_id2 = add_test_comment(editor, key2, "New Comment 2", cx);
30858
30859 assert_eq!(new_id1, 0, "ID counter should reset after take_all");
30860 assert_eq!(new_id2, 1, "IDs should be sequential after reset");
30861 });
30862}
30863
30864#[gpui::test]
30865fn test_diff_review_overlay_show_and_dismiss(cx: &mut TestAppContext) {
30866 init_test(cx, |_| {});
30867
30868 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30869
30870 // Show overlay
30871 editor
30872 .update(cx, |editor, window, cx| {
30873 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
30874 })
30875 .unwrap();
30876
30877 // Verify overlay is shown
30878 editor
30879 .update(cx, |editor, _window, cx| {
30880 assert!(!editor.diff_review_overlays.is_empty());
30881 assert_eq!(editor.diff_review_line_range(cx), Some((0, 0)));
30882 assert!(editor.diff_review_prompt_editor().is_some());
30883 })
30884 .unwrap();
30885
30886 // Dismiss overlay
30887 editor
30888 .update(cx, |editor, _window, cx| {
30889 editor.dismiss_all_diff_review_overlays(cx);
30890 })
30891 .unwrap();
30892
30893 // Verify overlay is dismissed
30894 editor
30895 .update(cx, |editor, _window, cx| {
30896 assert!(editor.diff_review_overlays.is_empty());
30897 assert_eq!(editor.diff_review_line_range(cx), None);
30898 assert!(editor.diff_review_prompt_editor().is_none());
30899 })
30900 .unwrap();
30901}
30902
30903#[gpui::test]
30904fn test_diff_review_overlay_dismiss_via_cancel(cx: &mut TestAppContext) {
30905 init_test(cx, |_| {});
30906
30907 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30908
30909 // Show overlay
30910 editor
30911 .update(cx, |editor, window, cx| {
30912 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
30913 })
30914 .unwrap();
30915
30916 // Verify overlay is shown
30917 editor
30918 .update(cx, |editor, _window, _cx| {
30919 assert!(!editor.diff_review_overlays.is_empty());
30920 })
30921 .unwrap();
30922
30923 // Dismiss via dismiss_menus_and_popups (which is called by cancel action)
30924 editor
30925 .update(cx, |editor, window, cx| {
30926 editor.dismiss_menus_and_popups(true, window, cx);
30927 })
30928 .unwrap();
30929
30930 // Verify overlay is dismissed
30931 editor
30932 .update(cx, |editor, _window, _cx| {
30933 assert!(editor.diff_review_overlays.is_empty());
30934 })
30935 .unwrap();
30936}
30937
30938#[gpui::test]
30939fn test_diff_review_empty_comment_not_submitted(cx: &mut TestAppContext) {
30940 init_test(cx, |_| {});
30941
30942 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30943
30944 // Show overlay
30945 editor
30946 .update(cx, |editor, window, cx| {
30947 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
30948 })
30949 .unwrap();
30950
30951 // Try to submit without typing anything (empty comment)
30952 editor
30953 .update(cx, |editor, window, cx| {
30954 editor.submit_diff_review_comment(window, cx);
30955 })
30956 .unwrap();
30957
30958 // Verify no comment was added
30959 editor
30960 .update(cx, |editor, _window, _cx| {
30961 assert_eq!(editor.total_review_comment_count(), 0);
30962 })
30963 .unwrap();
30964
30965 // Try to submit with whitespace-only comment
30966 editor
30967 .update(cx, |editor, window, cx| {
30968 if let Some(prompt_editor) = editor.diff_review_prompt_editor().cloned() {
30969 prompt_editor.update(cx, |pe, cx| {
30970 pe.insert(" \n\t ", window, cx);
30971 });
30972 }
30973 editor.submit_diff_review_comment(window, cx);
30974 })
30975 .unwrap();
30976
30977 // Verify still no comment was added
30978 editor
30979 .update(cx, |editor, _window, _cx| {
30980 assert_eq!(editor.total_review_comment_count(), 0);
30981 })
30982 .unwrap();
30983}
30984
30985#[gpui::test]
30986fn test_diff_review_inline_edit_flow(cx: &mut TestAppContext) {
30987 init_test(cx, |_| {});
30988
30989 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
30990
30991 // Add a comment directly
30992 let comment_id = editor
30993 .update(cx, |editor, _window, cx| {
30994 let key = test_hunk_key("");
30995 add_test_comment(editor, key, "Original comment", cx)
30996 })
30997 .unwrap();
30998
30999 // Set comment to editing mode
31000 editor
31001 .update(cx, |editor, _window, cx| {
31002 editor.set_comment_editing(comment_id, true, cx);
31003 })
31004 .unwrap();
31005
31006 // Verify editing flag is set
31007 editor
31008 .update(cx, |editor, _window, cx| {
31009 let key = test_hunk_key("");
31010 let snapshot = editor.buffer().read(cx).snapshot(cx);
31011 let comments = editor.comments_for_hunk(&key, &snapshot);
31012 assert_eq!(comments.len(), 1);
31013 assert!(comments[0].is_editing);
31014 })
31015 .unwrap();
31016
31017 // Update the comment
31018 editor
31019 .update(cx, |editor, _window, cx| {
31020 let updated =
31021 editor.update_review_comment(comment_id, "Updated comment".to_string(), cx);
31022 assert!(updated);
31023 })
31024 .unwrap();
31025
31026 // Verify comment was updated and editing flag is cleared
31027 editor
31028 .update(cx, |editor, _window, cx| {
31029 let key = test_hunk_key("");
31030 let snapshot = editor.buffer().read(cx).snapshot(cx);
31031 let comments = editor.comments_for_hunk(&key, &snapshot);
31032 assert_eq!(comments[0].comment, "Updated comment");
31033 assert!(!comments[0].is_editing);
31034 })
31035 .unwrap();
31036}
31037
31038#[gpui::test]
31039fn test_orphaned_comments_are_cleaned_up(cx: &mut TestAppContext) {
31040 init_test(cx, |_| {});
31041
31042 // Create an editor with some text
31043 let editor = cx.add_window(|window, cx| {
31044 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
31045 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31046 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
31047 });
31048
31049 // Add a comment with an anchor on line 2
31050 editor
31051 .update(cx, |editor, _window, cx| {
31052 let snapshot = editor.buffer().read(cx).snapshot(cx);
31053 let anchor = snapshot.anchor_after(Point::new(1, 0)); // Line 2
31054 let key = DiffHunkKey {
31055 file_path: Arc::from(util::rel_path::RelPath::empty()),
31056 hunk_start_anchor: anchor,
31057 };
31058 editor.add_review_comment(key, "Comment on line 2".to_string(), anchor..anchor, cx);
31059 assert_eq!(editor.total_review_comment_count(), 1);
31060 })
31061 .unwrap();
31062
31063 // Delete all content (this should orphan the comment's anchor)
31064 editor
31065 .update(cx, |editor, window, cx| {
31066 editor.select_all(&SelectAll, window, cx);
31067 editor.insert("completely new content", window, cx);
31068 })
31069 .unwrap();
31070
31071 // Trigger cleanup
31072 editor
31073 .update(cx, |editor, _window, cx| {
31074 editor.cleanup_orphaned_review_comments(cx);
31075 // Comment should be removed because its anchor is invalid
31076 assert_eq!(editor.total_review_comment_count(), 0);
31077 })
31078 .unwrap();
31079}
31080
31081#[gpui::test]
31082fn test_orphaned_comments_cleanup_called_on_buffer_edit(cx: &mut TestAppContext) {
31083 init_test(cx, |_| {});
31084
31085 // Create an editor with some text
31086 let editor = cx.add_window(|window, cx| {
31087 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
31088 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31089 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
31090 });
31091
31092 // Add a comment with an anchor on line 2
31093 editor
31094 .update(cx, |editor, _window, cx| {
31095 let snapshot = editor.buffer().read(cx).snapshot(cx);
31096 let anchor = snapshot.anchor_after(Point::new(1, 0)); // Line 2
31097 let key = DiffHunkKey {
31098 file_path: Arc::from(util::rel_path::RelPath::empty()),
31099 hunk_start_anchor: anchor,
31100 };
31101 editor.add_review_comment(key, "Comment on line 2".to_string(), anchor..anchor, cx);
31102 assert_eq!(editor.total_review_comment_count(), 1);
31103 })
31104 .unwrap();
31105
31106 // Edit the buffer - this should trigger cleanup via on_buffer_event
31107 // Delete all content which orphans the anchor
31108 editor
31109 .update(cx, |editor, window, cx| {
31110 editor.select_all(&SelectAll, window, cx);
31111 editor.insert("completely new content", window, cx);
31112 // The cleanup is called automatically in on_buffer_event when Edited fires
31113 })
31114 .unwrap();
31115
31116 // Verify cleanup happened automatically (not manually triggered)
31117 editor
31118 .update(cx, |editor, _window, _cx| {
31119 // Comment should be removed because its anchor became invalid
31120 // and cleanup was called automatically on buffer edit
31121 assert_eq!(editor.total_review_comment_count(), 0);
31122 })
31123 .unwrap();
31124}
31125
31126#[gpui::test]
31127fn test_comments_stored_for_multiple_hunks(cx: &mut TestAppContext) {
31128 init_test(cx, |_| {});
31129
31130 // This test verifies that comments can be stored for multiple different hunks
31131 // and that hunk_comment_count correctly identifies comments per hunk.
31132 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31133
31134 _ = editor.update(cx, |editor, _window, cx| {
31135 let snapshot = editor.buffer().read(cx).snapshot(cx);
31136
31137 // Create two different hunk keys (simulating two different files)
31138 let anchor = snapshot.anchor_before(Point::new(0, 0));
31139 let key1 = DiffHunkKey {
31140 file_path: Arc::from(util::rel_path::RelPath::unix("file1.rs").unwrap()),
31141 hunk_start_anchor: anchor,
31142 };
31143 let key2 = DiffHunkKey {
31144 file_path: Arc::from(util::rel_path::RelPath::unix("file2.rs").unwrap()),
31145 hunk_start_anchor: anchor,
31146 };
31147
31148 // Add comments to first hunk
31149 editor.add_review_comment(
31150 key1.clone(),
31151 "Comment 1 for file1".to_string(),
31152 anchor..anchor,
31153 cx,
31154 );
31155 editor.add_review_comment(
31156 key1.clone(),
31157 "Comment 2 for file1".to_string(),
31158 anchor..anchor,
31159 cx,
31160 );
31161
31162 // Add comment to second hunk
31163 editor.add_review_comment(
31164 key2.clone(),
31165 "Comment for file2".to_string(),
31166 anchor..anchor,
31167 cx,
31168 );
31169
31170 // Verify total count
31171 assert_eq!(editor.total_review_comment_count(), 3);
31172
31173 // Verify per-hunk counts
31174 let snapshot = editor.buffer().read(cx).snapshot(cx);
31175 assert_eq!(
31176 editor.hunk_comment_count(&key1, &snapshot),
31177 2,
31178 "file1 should have 2 comments"
31179 );
31180 assert_eq!(
31181 editor.hunk_comment_count(&key2, &snapshot),
31182 1,
31183 "file2 should have 1 comment"
31184 );
31185
31186 // Verify comments_for_hunk returns correct comments
31187 let file1_comments = editor.comments_for_hunk(&key1, &snapshot);
31188 assert_eq!(file1_comments.len(), 2);
31189 assert_eq!(file1_comments[0].comment, "Comment 1 for file1");
31190 assert_eq!(file1_comments[1].comment, "Comment 2 for file1");
31191
31192 let file2_comments = editor.comments_for_hunk(&key2, &snapshot);
31193 assert_eq!(file2_comments.len(), 1);
31194 assert_eq!(file2_comments[0].comment, "Comment for file2");
31195 });
31196}
31197
31198#[gpui::test]
31199fn test_same_hunk_detected_by_matching_keys(cx: &mut TestAppContext) {
31200 init_test(cx, |_| {});
31201
31202 // This test verifies that hunk_keys_match correctly identifies when two
31203 // DiffHunkKeys refer to the same hunk (same file path and anchor point).
31204 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31205
31206 _ = editor.update(cx, |editor, _window, cx| {
31207 let snapshot = editor.buffer().read(cx).snapshot(cx);
31208 let anchor = snapshot.anchor_before(Point::new(0, 0));
31209
31210 // Create two keys with the same file path and anchor
31211 let key1 = DiffHunkKey {
31212 file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
31213 hunk_start_anchor: anchor,
31214 };
31215 let key2 = DiffHunkKey {
31216 file_path: Arc::from(util::rel_path::RelPath::unix("file.rs").unwrap()),
31217 hunk_start_anchor: anchor,
31218 };
31219
31220 // Add comment to first key
31221 editor.add_review_comment(key1, "Test comment".to_string(), anchor..anchor, cx);
31222
31223 // Verify second key (same hunk) finds the comment
31224 let snapshot = editor.buffer().read(cx).snapshot(cx);
31225 assert_eq!(
31226 editor.hunk_comment_count(&key2, &snapshot),
31227 1,
31228 "Same hunk should find the comment"
31229 );
31230
31231 // Create a key with different file path
31232 let different_file_key = DiffHunkKey {
31233 file_path: Arc::from(util::rel_path::RelPath::unix("other.rs").unwrap()),
31234 hunk_start_anchor: anchor,
31235 };
31236
31237 // Different file should not find the comment
31238 assert_eq!(
31239 editor.hunk_comment_count(&different_file_key, &snapshot),
31240 0,
31241 "Different file should not find the comment"
31242 );
31243 });
31244}
31245
31246#[gpui::test]
31247fn test_overlay_comments_expanded_state(cx: &mut TestAppContext) {
31248 init_test(cx, |_| {});
31249
31250 // This test verifies that set_diff_review_comments_expanded correctly
31251 // updates the expanded state of overlays.
31252 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31253
31254 // Show overlay
31255 editor
31256 .update(cx, |editor, window, cx| {
31257 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(0), window, cx);
31258 })
31259 .unwrap();
31260
31261 // Verify initially expanded (default)
31262 editor
31263 .update(cx, |editor, _window, _cx| {
31264 assert!(
31265 editor.diff_review_overlays[0].comments_expanded,
31266 "Should be expanded by default"
31267 );
31268 })
31269 .unwrap();
31270
31271 // Set to collapsed using the public method
31272 editor
31273 .update(cx, |editor, _window, cx| {
31274 editor.set_diff_review_comments_expanded(false, cx);
31275 })
31276 .unwrap();
31277
31278 // Verify collapsed
31279 editor
31280 .update(cx, |editor, _window, _cx| {
31281 assert!(
31282 !editor.diff_review_overlays[0].comments_expanded,
31283 "Should be collapsed after setting to false"
31284 );
31285 })
31286 .unwrap();
31287
31288 // Set back to expanded
31289 editor
31290 .update(cx, |editor, _window, cx| {
31291 editor.set_diff_review_comments_expanded(true, cx);
31292 })
31293 .unwrap();
31294
31295 // Verify expanded again
31296 editor
31297 .update(cx, |editor, _window, _cx| {
31298 assert!(
31299 editor.diff_review_overlays[0].comments_expanded,
31300 "Should be expanded after setting to true"
31301 );
31302 })
31303 .unwrap();
31304}
31305
31306#[gpui::test]
31307fn test_diff_review_multiline_selection(cx: &mut TestAppContext) {
31308 init_test(cx, |_| {});
31309
31310 // Create an editor with multiple lines of text
31311 let editor = cx.add_window(|window, cx| {
31312 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\nline 4\nline 5\n", cx));
31313 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31314 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
31315 });
31316
31317 // Test showing overlay with a multi-line selection (lines 1-3, which are rows 0-2)
31318 editor
31319 .update(cx, |editor, window, cx| {
31320 editor.show_diff_review_overlay(DisplayRow(0)..DisplayRow(2), window, cx);
31321 })
31322 .unwrap();
31323
31324 // Verify line range
31325 editor
31326 .update(cx, |editor, _window, cx| {
31327 assert!(!editor.diff_review_overlays.is_empty());
31328 assert_eq!(editor.diff_review_line_range(cx), Some((0, 2)));
31329 })
31330 .unwrap();
31331
31332 // Dismiss and test with reversed range (end < start)
31333 editor
31334 .update(cx, |editor, _window, cx| {
31335 editor.dismiss_all_diff_review_overlays(cx);
31336 })
31337 .unwrap();
31338
31339 // Show overlay with reversed range - should normalize it
31340 editor
31341 .update(cx, |editor, window, cx| {
31342 editor.show_diff_review_overlay(DisplayRow(3)..DisplayRow(1), window, cx);
31343 })
31344 .unwrap();
31345
31346 // Verify range is normalized (start <= end)
31347 editor
31348 .update(cx, |editor, _window, cx| {
31349 assert_eq!(editor.diff_review_line_range(cx), Some((1, 3)));
31350 })
31351 .unwrap();
31352}
31353
31354#[gpui::test]
31355fn test_diff_review_drag_state(cx: &mut TestAppContext) {
31356 init_test(cx, |_| {});
31357
31358 let editor = cx.add_window(|window, cx| {
31359 let buffer = cx.new(|cx| Buffer::local("line 1\nline 2\nline 3\n", cx));
31360 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31361 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
31362 });
31363
31364 // Initially no drag state
31365 editor
31366 .update(cx, |editor, _window, _cx| {
31367 assert!(editor.diff_review_drag_state.is_none());
31368 })
31369 .unwrap();
31370
31371 // Start drag at row 1
31372 editor
31373 .update(cx, |editor, window, cx| {
31374 editor.start_diff_review_drag(DisplayRow(1), window, cx);
31375 })
31376 .unwrap();
31377
31378 // Verify drag state is set
31379 editor
31380 .update(cx, |editor, window, cx| {
31381 assert!(editor.diff_review_drag_state.is_some());
31382 let snapshot = editor.snapshot(window, cx);
31383 let range = editor
31384 .diff_review_drag_state
31385 .as_ref()
31386 .unwrap()
31387 .row_range(&snapshot.display_snapshot);
31388 assert_eq!(*range.start(), DisplayRow(1));
31389 assert_eq!(*range.end(), DisplayRow(1));
31390 })
31391 .unwrap();
31392
31393 // Update drag to row 3
31394 editor
31395 .update(cx, |editor, window, cx| {
31396 editor.update_diff_review_drag(DisplayRow(3), window, cx);
31397 })
31398 .unwrap();
31399
31400 // Verify drag state is updated
31401 editor
31402 .update(cx, |editor, window, cx| {
31403 assert!(editor.diff_review_drag_state.is_some());
31404 let snapshot = editor.snapshot(window, cx);
31405 let range = editor
31406 .diff_review_drag_state
31407 .as_ref()
31408 .unwrap()
31409 .row_range(&snapshot.display_snapshot);
31410 assert_eq!(*range.start(), DisplayRow(1));
31411 assert_eq!(*range.end(), DisplayRow(3));
31412 })
31413 .unwrap();
31414
31415 // End drag - should show overlay
31416 editor
31417 .update(cx, |editor, window, cx| {
31418 editor.end_diff_review_drag(window, cx);
31419 })
31420 .unwrap();
31421
31422 // Verify drag state is cleared and overlay is shown
31423 editor
31424 .update(cx, |editor, _window, cx| {
31425 assert!(editor.diff_review_drag_state.is_none());
31426 assert!(!editor.diff_review_overlays.is_empty());
31427 assert_eq!(editor.diff_review_line_range(cx), Some((1, 3)));
31428 })
31429 .unwrap();
31430}
31431
31432#[gpui::test]
31433fn test_diff_review_drag_cancel(cx: &mut TestAppContext) {
31434 init_test(cx, |_| {});
31435
31436 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31437
31438 // Start drag
31439 editor
31440 .update(cx, |editor, window, cx| {
31441 editor.start_diff_review_drag(DisplayRow(0), window, cx);
31442 })
31443 .unwrap();
31444
31445 // Verify drag state is set
31446 editor
31447 .update(cx, |editor, _window, _cx| {
31448 assert!(editor.diff_review_drag_state.is_some());
31449 })
31450 .unwrap();
31451
31452 // Cancel drag
31453 editor
31454 .update(cx, |editor, _window, cx| {
31455 editor.cancel_diff_review_drag(cx);
31456 })
31457 .unwrap();
31458
31459 // Verify drag state is cleared and no overlay was created
31460 editor
31461 .update(cx, |editor, _window, _cx| {
31462 assert!(editor.diff_review_drag_state.is_none());
31463 assert!(editor.diff_review_overlays.is_empty());
31464 })
31465 .unwrap();
31466}
31467
31468#[gpui::test]
31469fn test_calculate_overlay_height(cx: &mut TestAppContext) {
31470 init_test(cx, |_| {});
31471
31472 // This test verifies that calculate_overlay_height returns correct heights
31473 // based on comment count and expanded state.
31474 let editor = cx.add_window(|window, cx| Editor::single_line(window, cx));
31475
31476 _ = editor.update(cx, |editor, _window, cx| {
31477 let snapshot = editor.buffer().read(cx).snapshot(cx);
31478 let anchor = snapshot.anchor_before(Point::new(0, 0));
31479 let key = DiffHunkKey {
31480 file_path: Arc::from(util::rel_path::RelPath::empty()),
31481 hunk_start_anchor: anchor,
31482 };
31483
31484 // No comments: base height of 2
31485 let height_no_comments = editor.calculate_overlay_height(&key, true, &snapshot);
31486 assert_eq!(
31487 height_no_comments, 2,
31488 "Base height should be 2 with no comments"
31489 );
31490
31491 // Add one comment
31492 editor.add_review_comment(key.clone(), "Comment 1".to_string(), anchor..anchor, cx);
31493
31494 let snapshot = editor.buffer().read(cx).snapshot(cx);
31495
31496 // With comments expanded: base (2) + header (1) + 2 per comment
31497 let height_expanded = editor.calculate_overlay_height(&key, true, &snapshot);
31498 assert_eq!(
31499 height_expanded,
31500 2 + 1 + 2, // base + header + 1 comment * 2
31501 "Height with 1 comment expanded"
31502 );
31503
31504 // With comments collapsed: base (2) + header (1)
31505 let height_collapsed = editor.calculate_overlay_height(&key, false, &snapshot);
31506 assert_eq!(
31507 height_collapsed,
31508 2 + 1, // base + header only
31509 "Height with comments collapsed"
31510 );
31511
31512 // Add more comments
31513 editor.add_review_comment(key.clone(), "Comment 2".to_string(), anchor..anchor, cx);
31514 editor.add_review_comment(key.clone(), "Comment 3".to_string(), anchor..anchor, cx);
31515
31516 let snapshot = editor.buffer().read(cx).snapshot(cx);
31517
31518 // With 3 comments expanded
31519 let height_3_expanded = editor.calculate_overlay_height(&key, true, &snapshot);
31520 assert_eq!(
31521 height_3_expanded,
31522 2 + 1 + (3 * 2), // base + header + 3 comments * 2
31523 "Height with 3 comments expanded"
31524 );
31525
31526 // Collapsed height stays the same regardless of comment count
31527 let height_3_collapsed = editor.calculate_overlay_height(&key, false, &snapshot);
31528 assert_eq!(
31529 height_3_collapsed,
31530 2 + 1, // base + header only
31531 "Height with 3 comments collapsed should be same as 1 comment collapsed"
31532 );
31533 });
31534}
31535
31536#[gpui::test]
31537async fn test_move_to_start_end_of_larger_syntax_node_single_cursor(cx: &mut TestAppContext) {
31538 init_test(cx, |_| {});
31539
31540 let language = Arc::new(Language::new(
31541 LanguageConfig::default(),
31542 Some(tree_sitter_rust::LANGUAGE.into()),
31543 ));
31544
31545 let text = r#"
31546 fn main() {
31547 let x = foo(1, 2);
31548 }
31549 "#
31550 .unindent();
31551
31552 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
31553 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31554 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
31555
31556 editor
31557 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
31558 .await;
31559
31560 // Test case 1: Move to end of syntax nodes
31561 editor.update_in(cx, |editor, window, cx| {
31562 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31563 s.select_display_ranges([
31564 DisplayPoint::new(DisplayRow(1), 16)..DisplayPoint::new(DisplayRow(1), 16)
31565 ]);
31566 });
31567 });
31568 editor.update(cx, |editor, cx| {
31569 assert_text_with_selections(
31570 editor,
31571 indoc! {r#"
31572 fn main() {
31573 let x = foo(ˇ1, 2);
31574 }
31575 "#},
31576 cx,
31577 );
31578 });
31579 editor.update_in(cx, |editor, window, cx| {
31580 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31581 });
31582 editor.update(cx, |editor, cx| {
31583 assert_text_with_selections(
31584 editor,
31585 indoc! {r#"
31586 fn main() {
31587 let x = foo(1ˇ, 2);
31588 }
31589 "#},
31590 cx,
31591 );
31592 });
31593 editor.update_in(cx, |editor, window, cx| {
31594 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31595 });
31596 editor.update(cx, |editor, cx| {
31597 assert_text_with_selections(
31598 editor,
31599 indoc! {r#"
31600 fn main() {
31601 let x = foo(1, 2)ˇ;
31602 }
31603 "#},
31604 cx,
31605 );
31606 });
31607 editor.update_in(cx, |editor, window, cx| {
31608 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31609 });
31610 editor.update(cx, |editor, cx| {
31611 assert_text_with_selections(
31612 editor,
31613 indoc! {r#"
31614 fn main() {
31615 let x = foo(1, 2);ˇ
31616 }
31617 "#},
31618 cx,
31619 );
31620 });
31621 editor.update_in(cx, |editor, window, cx| {
31622 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31623 });
31624 editor.update(cx, |editor, cx| {
31625 assert_text_with_selections(
31626 editor,
31627 indoc! {r#"
31628 fn main() {
31629 let x = foo(1, 2);
31630 }ˇ
31631 "#},
31632 cx,
31633 );
31634 });
31635
31636 // Test case 2: Move to start of syntax nodes
31637 editor.update_in(cx, |editor, window, cx| {
31638 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31639 s.select_display_ranges([
31640 DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20)
31641 ]);
31642 });
31643 });
31644 editor.update(cx, |editor, cx| {
31645 assert_text_with_selections(
31646 editor,
31647 indoc! {r#"
31648 fn main() {
31649 let x = foo(1, 2ˇ);
31650 }
31651 "#},
31652 cx,
31653 );
31654 });
31655 editor.update_in(cx, |editor, window, cx| {
31656 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31657 });
31658 editor.update(cx, |editor, cx| {
31659 assert_text_with_selections(
31660 editor,
31661 indoc! {r#"
31662 fn main() {
31663 let x = fooˇ(1, 2);
31664 }
31665 "#},
31666 cx,
31667 );
31668 });
31669 editor.update_in(cx, |editor, window, cx| {
31670 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31671 });
31672 editor.update(cx, |editor, cx| {
31673 assert_text_with_selections(
31674 editor,
31675 indoc! {r#"
31676 fn main() {
31677 let x = ˇfoo(1, 2);
31678 }
31679 "#},
31680 cx,
31681 );
31682 });
31683 editor.update_in(cx, |editor, window, cx| {
31684 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31685 });
31686 editor.update(cx, |editor, cx| {
31687 assert_text_with_selections(
31688 editor,
31689 indoc! {r#"
31690 fn main() {
31691 ˇlet x = foo(1, 2);
31692 }
31693 "#},
31694 cx,
31695 );
31696 });
31697 editor.update_in(cx, |editor, window, cx| {
31698 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31699 });
31700 editor.update(cx, |editor, cx| {
31701 assert_text_with_selections(
31702 editor,
31703 indoc! {r#"
31704 fn main() ˇ{
31705 let x = foo(1, 2);
31706 }
31707 "#},
31708 cx,
31709 );
31710 });
31711 editor.update_in(cx, |editor, window, cx| {
31712 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31713 });
31714 editor.update(cx, |editor, cx| {
31715 assert_text_with_selections(
31716 editor,
31717 indoc! {r#"
31718 ˇfn main() {
31719 let x = foo(1, 2);
31720 }
31721 "#},
31722 cx,
31723 );
31724 });
31725}
31726
31727#[gpui::test]
31728async fn test_move_to_start_end_of_larger_syntax_node_two_cursors(cx: &mut TestAppContext) {
31729 init_test(cx, |_| {});
31730
31731 let language = Arc::new(Language::new(
31732 LanguageConfig::default(),
31733 Some(tree_sitter_rust::LANGUAGE.into()),
31734 ));
31735
31736 let text = r#"
31737 fn main() {
31738 let x = foo(1, 2);
31739 let y = bar(3, 4);
31740 }
31741 "#
31742 .unindent();
31743
31744 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
31745 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31746 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
31747
31748 editor
31749 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
31750 .await;
31751
31752 // Test case 1: Move to end of syntax nodes with two cursors
31753 editor.update_in(cx, |editor, window, cx| {
31754 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31755 s.select_display_ranges([
31756 DisplayPoint::new(DisplayRow(1), 20)..DisplayPoint::new(DisplayRow(1), 20),
31757 DisplayPoint::new(DisplayRow(2), 20)..DisplayPoint::new(DisplayRow(2), 20),
31758 ]);
31759 });
31760 });
31761 editor.update(cx, |editor, cx| {
31762 assert_text_with_selections(
31763 editor,
31764 indoc! {r#"
31765 fn main() {
31766 let x = foo(1, 2ˇ);
31767 let y = bar(3, 4ˇ);
31768 }
31769 "#},
31770 cx,
31771 );
31772 });
31773 editor.update_in(cx, |editor, window, cx| {
31774 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31775 });
31776 editor.update(cx, |editor, cx| {
31777 assert_text_with_selections(
31778 editor,
31779 indoc! {r#"
31780 fn main() {
31781 let x = foo(1, 2)ˇ;
31782 let y = bar(3, 4)ˇ;
31783 }
31784 "#},
31785 cx,
31786 );
31787 });
31788 editor.update_in(cx, |editor, window, cx| {
31789 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31790 });
31791 editor.update(cx, |editor, cx| {
31792 assert_text_with_selections(
31793 editor,
31794 indoc! {r#"
31795 fn main() {
31796 let x = foo(1, 2);ˇ
31797 let y = bar(3, 4);ˇ
31798 }
31799 "#},
31800 cx,
31801 );
31802 });
31803
31804 // Test case 2: Move to start of syntax nodes with two cursors
31805 editor.update_in(cx, |editor, window, cx| {
31806 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31807 s.select_display_ranges([
31808 DisplayPoint::new(DisplayRow(1), 19)..DisplayPoint::new(DisplayRow(1), 19),
31809 DisplayPoint::new(DisplayRow(2), 19)..DisplayPoint::new(DisplayRow(2), 19),
31810 ]);
31811 });
31812 });
31813 editor.update(cx, |editor, cx| {
31814 assert_text_with_selections(
31815 editor,
31816 indoc! {r#"
31817 fn main() {
31818 let x = foo(1, ˇ2);
31819 let y = bar(3, ˇ4);
31820 }
31821 "#},
31822 cx,
31823 );
31824 });
31825 editor.update_in(cx, |editor, window, cx| {
31826 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31827 });
31828 editor.update(cx, |editor, cx| {
31829 assert_text_with_selections(
31830 editor,
31831 indoc! {r#"
31832 fn main() {
31833 let x = fooˇ(1, 2);
31834 let y = barˇ(3, 4);
31835 }
31836 "#},
31837 cx,
31838 );
31839 });
31840 editor.update_in(cx, |editor, window, cx| {
31841 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31842 });
31843 editor.update(cx, |editor, cx| {
31844 assert_text_with_selections(
31845 editor,
31846 indoc! {r#"
31847 fn main() {
31848 let x = ˇfoo(1, 2);
31849 let y = ˇbar(3, 4);
31850 }
31851 "#},
31852 cx,
31853 );
31854 });
31855 editor.update_in(cx, |editor, window, cx| {
31856 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31857 });
31858 editor.update(cx, |editor, cx| {
31859 assert_text_with_selections(
31860 editor,
31861 indoc! {r#"
31862 fn main() {
31863 ˇlet x = foo(1, 2);
31864 ˇlet y = bar(3, 4);
31865 }
31866 "#},
31867 cx,
31868 );
31869 });
31870}
31871
31872#[gpui::test]
31873async fn test_move_to_start_end_of_larger_syntax_node_with_selections_and_strings(
31874 cx: &mut TestAppContext,
31875) {
31876 init_test(cx, |_| {});
31877
31878 let language = Arc::new(Language::new(
31879 LanguageConfig::default(),
31880 Some(tree_sitter_rust::LANGUAGE.into()),
31881 ));
31882
31883 let text = r#"
31884 fn main() {
31885 let x = foo(1, 2);
31886 let msg = "hello world";
31887 }
31888 "#
31889 .unindent();
31890
31891 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
31892 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
31893 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
31894
31895 editor
31896 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
31897 .await;
31898
31899 // Test case 1: With existing selection, move_to_end keeps selection
31900 editor.update_in(cx, |editor, window, cx| {
31901 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31902 s.select_display_ranges([
31903 DisplayPoint::new(DisplayRow(1), 12)..DisplayPoint::new(DisplayRow(1), 21)
31904 ]);
31905 });
31906 });
31907 editor.update(cx, |editor, cx| {
31908 assert_text_with_selections(
31909 editor,
31910 indoc! {r#"
31911 fn main() {
31912 let x = «foo(1, 2)ˇ»;
31913 let msg = "hello world";
31914 }
31915 "#},
31916 cx,
31917 );
31918 });
31919 editor.update_in(cx, |editor, window, cx| {
31920 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31921 });
31922 editor.update(cx, |editor, cx| {
31923 assert_text_with_selections(
31924 editor,
31925 indoc! {r#"
31926 fn main() {
31927 let x = «foo(1, 2)ˇ»;
31928 let msg = "hello world";
31929 }
31930 "#},
31931 cx,
31932 );
31933 });
31934
31935 // Test case 2: Move to end within a string
31936 editor.update_in(cx, |editor, window, cx| {
31937 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31938 s.select_display_ranges([
31939 DisplayPoint::new(DisplayRow(2), 15)..DisplayPoint::new(DisplayRow(2), 15)
31940 ]);
31941 });
31942 });
31943 editor.update(cx, |editor, cx| {
31944 assert_text_with_selections(
31945 editor,
31946 indoc! {r#"
31947 fn main() {
31948 let x = foo(1, 2);
31949 let msg = "ˇhello world";
31950 }
31951 "#},
31952 cx,
31953 );
31954 });
31955 editor.update_in(cx, |editor, window, cx| {
31956 editor.move_to_end_of_larger_syntax_node(&MoveToEndOfLargerSyntaxNode, window, cx);
31957 });
31958 editor.update(cx, |editor, cx| {
31959 assert_text_with_selections(
31960 editor,
31961 indoc! {r#"
31962 fn main() {
31963 let x = foo(1, 2);
31964 let msg = "hello worldˇ";
31965 }
31966 "#},
31967 cx,
31968 );
31969 });
31970
31971 // Test case 3: Move to start within a string
31972 editor.update_in(cx, |editor, window, cx| {
31973 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
31974 s.select_display_ranges([
31975 DisplayPoint::new(DisplayRow(2), 21)..DisplayPoint::new(DisplayRow(2), 21)
31976 ]);
31977 });
31978 });
31979 editor.update(cx, |editor, cx| {
31980 assert_text_with_selections(
31981 editor,
31982 indoc! {r#"
31983 fn main() {
31984 let x = foo(1, 2);
31985 let msg = "hello ˇworld";
31986 }
31987 "#},
31988 cx,
31989 );
31990 });
31991 editor.update_in(cx, |editor, window, cx| {
31992 editor.move_to_start_of_larger_syntax_node(&MoveToStartOfLargerSyntaxNode, window, cx);
31993 });
31994 editor.update(cx, |editor, cx| {
31995 assert_text_with_selections(
31996 editor,
31997 indoc! {r#"
31998 fn main() {
31999 let x = foo(1, 2);
32000 let msg = "ˇhello world";
32001 }
32002 "#},
32003 cx,
32004 );
32005 });
32006}
32007
32008#[gpui::test]
32009async fn test_select_to_start_end_of_larger_syntax_node(cx: &mut TestAppContext) {
32010 init_test(cx, |_| {});
32011
32012 let language = Arc::new(Language::new(
32013 LanguageConfig::default(),
32014 Some(tree_sitter_rust::LANGUAGE.into()),
32015 ));
32016
32017 // Test Group 1.1: Cursor in String - First Jump (Select to End)
32018 let text = r#"let msg = "foo bar baz";"#.unindent();
32019
32020 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32021 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32022 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32023
32024 editor
32025 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32026 .await;
32027
32028 editor.update_in(cx, |editor, window, cx| {
32029 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32030 s.select_display_ranges([
32031 DisplayPoint::new(DisplayRow(0), 14)..DisplayPoint::new(DisplayRow(0), 14)
32032 ]);
32033 });
32034 });
32035 editor.update(cx, |editor, cx| {
32036 assert_text_with_selections(editor, indoc! {r#"let msg = "fooˇ bar baz";"#}, cx);
32037 });
32038 editor.update_in(cx, |editor, window, cx| {
32039 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32040 });
32041 editor.update(cx, |editor, cx| {
32042 assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar bazˇ»";"#}, cx);
32043 });
32044
32045 // Test Group 1.2: Cursor in String - Second Jump (Select to End)
32046 editor.update_in(cx, |editor, window, cx| {
32047 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32048 });
32049 editor.update(cx, |editor, cx| {
32050 assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar baz"ˇ»;"#}, cx);
32051 });
32052
32053 // Test Group 1.3: Cursor in String - Third Jump (Select to End)
32054 editor.update_in(cx, |editor, window, cx| {
32055 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32056 });
32057 editor.update(cx, |editor, cx| {
32058 assert_text_with_selections(editor, indoc! {r#"let msg = "foo« bar baz";ˇ»"#}, cx);
32059 });
32060
32061 // Test Group 1.4: Cursor in String - First Jump (Select to Start)
32062 editor.update_in(cx, |editor, window, cx| {
32063 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32064 s.select_display_ranges([
32065 DisplayPoint::new(DisplayRow(0), 18)..DisplayPoint::new(DisplayRow(0), 18)
32066 ]);
32067 });
32068 });
32069 editor.update(cx, |editor, cx| {
32070 assert_text_with_selections(editor, indoc! {r#"let msg = "foo barˇ baz";"#}, cx);
32071 });
32072 editor.update_in(cx, |editor, window, cx| {
32073 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
32074 });
32075 editor.update(cx, |editor, cx| {
32076 assert_text_with_selections(editor, indoc! {r#"let msg = "«ˇfoo bar» baz";"#}, cx);
32077 });
32078
32079 // Test Group 1.5: Cursor in String - Second Jump (Select to Start)
32080 editor.update_in(cx, |editor, window, cx| {
32081 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
32082 });
32083 editor.update(cx, |editor, cx| {
32084 assert_text_with_selections(editor, indoc! {r#"let msg = «ˇ"foo bar» baz";"#}, cx);
32085 });
32086
32087 // Test Group 1.6: Cursor in String - Third Jump (Select to Start)
32088 editor.update_in(cx, |editor, window, cx| {
32089 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
32090 });
32091 editor.update(cx, |editor, cx| {
32092 assert_text_with_selections(editor, indoc! {r#"«ˇlet msg = "foo bar» baz";"#}, cx);
32093 });
32094
32095 // Test Group 2.1: Let Statement Progression (Select to End)
32096 let text = r#"
32097fn main() {
32098 let x = "hello";
32099}
32100"#
32101 .unindent();
32102
32103 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32104 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32105 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32106
32107 editor
32108 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32109 .await;
32110
32111 editor.update_in(cx, |editor, window, cx| {
32112 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32113 s.select_display_ranges([
32114 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)
32115 ]);
32116 });
32117 });
32118 editor.update(cx, |editor, cx| {
32119 assert_text_with_selections(
32120 editor,
32121 indoc! {r#"
32122 fn main() {
32123 let xˇ = "hello";
32124 }
32125 "#},
32126 cx,
32127 );
32128 });
32129 editor.update_in(cx, |editor, window, cx| {
32130 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32131 });
32132 editor.update(cx, |editor, cx| {
32133 assert_text_with_selections(
32134 editor,
32135 indoc! {r##"
32136 fn main() {
32137 let x« = "hello";ˇ»
32138 }
32139 "##},
32140 cx,
32141 );
32142 });
32143 editor.update_in(cx, |editor, window, cx| {
32144 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32145 });
32146 editor.update(cx, |editor, cx| {
32147 assert_text_with_selections(
32148 editor,
32149 indoc! {r#"
32150 fn main() {
32151 let x« = "hello";
32152 }ˇ»
32153 "#},
32154 cx,
32155 );
32156 });
32157
32158 // Test Group 2.2a: From Inside String Content Node To String Content Boundary
32159 let text = r#"let x = "hello";"#.unindent();
32160
32161 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32162 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32163 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32164
32165 editor
32166 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32167 .await;
32168
32169 editor.update_in(cx, |editor, window, cx| {
32170 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32171 s.select_display_ranges([
32172 DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12)
32173 ]);
32174 });
32175 });
32176 editor.update(cx, |editor, cx| {
32177 assert_text_with_selections(editor, indoc! {r#"let x = "helˇlo";"#}, cx);
32178 });
32179 editor.update_in(cx, |editor, window, cx| {
32180 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
32181 });
32182 editor.update(cx, |editor, cx| {
32183 assert_text_with_selections(editor, indoc! {r#"let x = "«ˇhel»lo";"#}, cx);
32184 });
32185
32186 // Test Group 2.2b: From Edge of String Content Node To String Literal Boundary
32187 editor.update_in(cx, |editor, window, cx| {
32188 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32189 s.select_display_ranges([
32190 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
32191 ]);
32192 });
32193 });
32194 editor.update(cx, |editor, cx| {
32195 assert_text_with_selections(editor, indoc! {r#"let x = "ˇhello";"#}, cx);
32196 });
32197 editor.update_in(cx, |editor, window, cx| {
32198 editor.select_to_start_of_larger_syntax_node(&SelectToStartOfLargerSyntaxNode, window, cx);
32199 });
32200 editor.update(cx, |editor, cx| {
32201 assert_text_with_selections(editor, indoc! {r#"let x = «ˇ"»hello";"#}, cx);
32202 });
32203
32204 // Test Group 3.1: Create Selection from Cursor (Select to End)
32205 let text = r#"let x = "hello world";"#.unindent();
32206
32207 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32208 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32209 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32210
32211 editor
32212 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32213 .await;
32214
32215 editor.update_in(cx, |editor, window, cx| {
32216 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32217 s.select_display_ranges([
32218 DisplayPoint::new(DisplayRow(0), 14)..DisplayPoint::new(DisplayRow(0), 14)
32219 ]);
32220 });
32221 });
32222 editor.update(cx, |editor, cx| {
32223 assert_text_with_selections(editor, indoc! {r#"let x = "helloˇ world";"#}, cx);
32224 });
32225 editor.update_in(cx, |editor, window, cx| {
32226 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32227 });
32228 editor.update(cx, |editor, cx| {
32229 assert_text_with_selections(editor, indoc! {r#"let x = "hello« worldˇ»";"#}, cx);
32230 });
32231
32232 // Test Group 3.2: Extend Existing Selection (Select to End)
32233 editor.update_in(cx, |editor, window, cx| {
32234 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32235 s.select_display_ranges([
32236 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 17)
32237 ]);
32238 });
32239 });
32240 editor.update(cx, |editor, cx| {
32241 assert_text_with_selections(editor, indoc! {r#"let x = "he«llo woˇ»rld";"#}, cx);
32242 });
32243 editor.update_in(cx, |editor, window, cx| {
32244 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32245 });
32246 editor.update(cx, |editor, cx| {
32247 assert_text_with_selections(editor, indoc! {r#"let x = "he«llo worldˇ»";"#}, cx);
32248 });
32249
32250 // Test Group 4.1: Multiple Cursors - All Expand to Different Syntax Nodes
32251 let text = r#"let x = "hello"; let y = 42;"#.unindent();
32252
32253 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32254 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32255 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32256
32257 editor
32258 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32259 .await;
32260
32261 editor.update_in(cx, |editor, window, cx| {
32262 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32263 s.select_display_ranges([
32264 // Cursor inside string content
32265 DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12),
32266 // Cursor at let statement semicolon
32267 DisplayPoint::new(DisplayRow(0), 18)..DisplayPoint::new(DisplayRow(0), 18),
32268 // Cursor inside integer literal
32269 DisplayPoint::new(DisplayRow(0), 26)..DisplayPoint::new(DisplayRow(0), 26),
32270 ]);
32271 });
32272 });
32273 editor.update(cx, |editor, cx| {
32274 assert_text_with_selections(editor, indoc! {r#"let x = "helˇlo"; lˇet y = 4ˇ2;"#}, cx);
32275 });
32276 editor.update_in(cx, |editor, window, cx| {
32277 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32278 });
32279 editor.update(cx, |editor, cx| {
32280 assert_text_with_selections(editor, indoc! {r#"let x = "hel«loˇ»"; l«et y = 42;ˇ»"#}, cx);
32281 });
32282
32283 // Test Group 4.2: Multiple Cursors on Separate Lines
32284 let text = r#"
32285let x = "hello";
32286let y = 42;
32287"#
32288 .unindent();
32289
32290 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32291 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32292 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32293
32294 editor
32295 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32296 .await;
32297
32298 editor.update_in(cx, |editor, window, cx| {
32299 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32300 s.select_display_ranges([
32301 DisplayPoint::new(DisplayRow(0), 12)..DisplayPoint::new(DisplayRow(0), 12),
32302 DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9),
32303 ]);
32304 });
32305 });
32306
32307 editor.update(cx, |editor, cx| {
32308 assert_text_with_selections(
32309 editor,
32310 indoc! {r#"
32311 let x = "helˇlo";
32312 let y = 4ˇ2;
32313 "#},
32314 cx,
32315 );
32316 });
32317 editor.update_in(cx, |editor, window, cx| {
32318 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32319 });
32320 editor.update(cx, |editor, cx| {
32321 assert_text_with_selections(
32322 editor,
32323 indoc! {r#"
32324 let x = "hel«loˇ»";
32325 let y = 4«2ˇ»;
32326 "#},
32327 cx,
32328 );
32329 });
32330
32331 // Test Group 5.1: Nested Function Calls
32332 let text = r#"let result = foo(bar("arg"));"#.unindent();
32333
32334 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32335 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32336 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32337
32338 editor
32339 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32340 .await;
32341
32342 editor.update_in(cx, |editor, window, cx| {
32343 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32344 s.select_display_ranges([
32345 DisplayPoint::new(DisplayRow(0), 22)..DisplayPoint::new(DisplayRow(0), 22)
32346 ]);
32347 });
32348 });
32349 editor.update(cx, |editor, cx| {
32350 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("ˇarg"));"#}, cx);
32351 });
32352 editor.update_in(cx, |editor, window, cx| {
32353 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32354 });
32355 editor.update(cx, |editor, cx| {
32356 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«argˇ»"));"#}, cx);
32357 });
32358 editor.update_in(cx, |editor, window, cx| {
32359 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32360 });
32361 editor.update(cx, |editor, cx| {
32362 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«arg"ˇ»));"#}, cx);
32363 });
32364 editor.update_in(cx, |editor, window, cx| {
32365 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32366 });
32367 editor.update(cx, |editor, cx| {
32368 assert_text_with_selections(editor, indoc! {r#"let result = foo(bar("«arg")ˇ»);"#}, cx);
32369 });
32370
32371 // Test Group 6.1: Block Comments
32372 let text = r#"let x = /* multi
32373 line
32374 comment */;"#
32375 .unindent();
32376
32377 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32378 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32379 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32380
32381 editor
32382 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32383 .await;
32384
32385 editor.update_in(cx, |editor, window, cx| {
32386 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32387 s.select_display_ranges([
32388 DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16)
32389 ]);
32390 });
32391 });
32392 editor.update(cx, |editor, cx| {
32393 assert_text_with_selections(
32394 editor,
32395 indoc! {r#"
32396let x = /* multiˇ
32397line
32398comment */;"#},
32399 cx,
32400 );
32401 });
32402 editor.update_in(cx, |editor, window, cx| {
32403 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32404 });
32405 editor.update(cx, |editor, cx| {
32406 assert_text_with_selections(
32407 editor,
32408 indoc! {r#"
32409let x = /* multi«
32410line
32411comment */ˇ»;"#},
32412 cx,
32413 );
32414 });
32415
32416 // Test Group 6.2: Array/Vector Literals
32417 let text = r#"let arr = [1, 2, 3];"#.unindent();
32418
32419 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language.clone(), cx));
32420 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
32421 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
32422
32423 editor
32424 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
32425 .await;
32426
32427 editor.update_in(cx, |editor, window, cx| {
32428 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
32429 s.select_display_ranges([
32430 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
32431 ]);
32432 });
32433 });
32434 editor.update(cx, |editor, cx| {
32435 assert_text_with_selections(editor, indoc! {r#"let arr = [ˇ1, 2, 3];"#}, cx);
32436 });
32437 editor.update_in(cx, |editor, window, cx| {
32438 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32439 });
32440 editor.update(cx, |editor, cx| {
32441 assert_text_with_selections(editor, indoc! {r#"let arr = [«1ˇ», 2, 3];"#}, cx);
32442 });
32443 editor.update_in(cx, |editor, window, cx| {
32444 editor.select_to_end_of_larger_syntax_node(&SelectToEndOfLargerSyntaxNode, window, cx);
32445 });
32446 editor.update(cx, |editor, cx| {
32447 assert_text_with_selections(editor, indoc! {r#"let arr = [«1, 2, 3]ˇ»;"#}, cx);
32448 });
32449}